
Tenda AC15 राउटर फर्मवेयर पुनः होस्टिंग और दूरस्थ कमांड निष्पादन (CVE-2020-10987) शोषण प्रतिकृति के लिए लेख
यह लेख दिखाता है कि कैसे मैंने AC15 (V15.03.05.19) फर्मवेयर के वेबसर्वर को QEMU के साथ एमुलेट किया, इसे होस्ट ब्राउज़र से पहुंच योग्य बनाया, और कमांड निष्पादन प्राप्त करने के लिए एमुलेटेड rootfs के अंदर /goform/setUsbUnload हैंडलर (CVE-2020-10987) का उपयोग किया।
squashfs फाइलसिस्टम निकालें (या हमारे मामले में प्राप्त करें) और रिवर्सिंग शुरू करेंपहले हमें अपनी फर्मवेयर इमेज प्राप्त करनी होगी। मैं AC15 V15.03.05.19 के लिए इमेज डाउनलोड नहीं ढूंढ पाया, हालांकि मुझे पहले से निकाले गए squashfs फाइलसिस्टम के साथ एक github रेपो मिल गया। यदि हमारे पास सही इमेज फ़ाइल होती, तो हम इसे binwalk का उपयोग करके निकाल सकते थे, जैसे,```
user@computer $ binwalk -e AC15_V15.03.05.19.bin
64 0x40 TRX firmware header, little endian, image size: 6778880 bytes, CRC32: 0x80AD82D6, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x1A488C, rootfs offset: 0x0 92 0x5C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4177792 bytes 1722572 0x1A48CC Squashfs filesystem, little endian, version 4.0, compression:xz, size: 5052332 bytes, 848 inodes, blocksize: 131072 bytes, created: 2017-04-19 16:18:08
user@computer $ cd _AC15_V15.03.05.19.bin.extracted
चूंकि हमारे पास सही इमेज फ़ाइल नहीं है, लेकिन पहले से निकाली गई फाइलसिस्टम वाला एक रेपो है, हम बस रेपो को क्लोन कर सकते हैं।```
git clone https://github.com/lapinpt/Tenda-AC15-Firmware-V15.03.05.19-9061
मैंने VR नामक एक डायरेक्टरी बनाई है और इस रिपॉजिटरी को इसके अंदर क्लोन किया है। मेरा rootfs $HOME/VR/Tenda-AC15-Firmware-V15.03.05.19-9061/rootfs पर है।
बढ़िया, अब हमारे पास हमारा AC15 V15.03.05.19 फर्मवेयर है। चलिए कुछ रिवर्सिंग करते हैं ताकि देख सकें कि क्या हो रहा है।
मैं अपने रिवर्सिंग के लिए Ghidra 11.4.2 का उपयोग करूंगा।
चलिए अपने टार्गेट बाइनरी rootfs/bin/httpd पर जाते हैं और इसे Ghidra में लोड करते हैं।
अगर हम formsetUsbUnload फंक्शन पर जाते हैं, तो हम देख सकते हैं,```C
uVar1 = FUN_0002bd4c(param_1,"deviceName",&DAT_000f4bdc);
doSystemCmd("cfm post netctrl %d?op=%d,string_info=%s",0x33,3,uVar1);
FUN_0002c6cc(param_1,"HTTP/1.0 200 OK\r\n\r\n");
FUN_0002c6cc(param_1,"{"errCode":0}");
FUN_0002cc14(param_1,200);
return;
यह कमज़ोरी है। `deviceName` पैरामीटर सीधे `doSystemCmd` में पास किया जाता है जिससे हम जो चाहें कमांड भेज सकते हैं।
अब चूँकि हम इस फर्मवेयर को qemu का उपयोग करके रीहोस्ट करने जा रहे हैं, न कि मूल राउटर हार्डवेयर से, कुछ प्रोग्राम उन डिवाइसों तक पहुँचने का प्रयास करेंगे जो वहाँ नहीं हैं और हमारे स्टार्टअप को क्रैश कर देंगे।
चूँकि हमारा लक्ष्य वेबसर्वर (`httpd`) का शोषण करना है, मैंने केवल उस बाइनरी को रीहोस्ट करने पर ध्यान केंद्रित किया, न कि पूरे स्टार्टअप (`/rootfs/etc_ro/init.d/rcS`) पर। _पीछे मुड़कर देखने पर मुझे यकीन नहीं है कि यह सही कदम था_
इसलिए `rcS` को देखते समय मैं कुछ ऐसा खोजना चाहता था जो `rcS` कर सकता है और `httpd` को इसकी आवश्यकता होगी। फ़ाइल के अंत की ओर हम देख सकते हैं:```bash
cfmd &
echo '' > /proc/sys/kernel/hotplug
udevd &
logserver &
rcS शेष स्टैक के शुरू होने से ठीक पहले पृष्ठभूमि में cfmd प्रारंभ करता है।
कुछ और शोध करने और यह देखने के बाद कि कमजोर फ़ंक्शन कमांड कैसे भेजता है, मैं इस निष्कर्ष पर पहुंचा कि,
httpd एक cfm post बनाता हैcfm क्लाइंट एक UNIX डोमेन सॉकेट (जैसे /var/cfm_socket) के माध्यम से cfmd से संवाद करता है।cfmd में InitServer रूटीन में हम देख सकते हैं```C
unlink("/var/cfm_socket");
strncpy(sa_unix.sun_path, "/var/cfm_socket", ...);
bind(fd, (sockaddr*)&sa_unix, 0x6e);
listen(fd, 5);
यह एक UIX socket बनाता है और सुनता है।
कुल मिलाकर यह कैसे काम करता है कि handler प्रत्येक क्लाइंट से एक निश्चित 0x7e0-बाइट frame पढ़ता है (`RecvMsg`/`SendMsg`)। पहले 4 बाइट एक command code होते हैं; फिर एक 512-बाइट key buffer और एक 1500-बाइट value buffer होता है (आप handler में stack object sizes देख सकते हैं)। यह opcode पर switch करता है और एक ACK code के साथ उत्तर देता है:
- `2` -> Get: `GetCfmValue(key, value)` फिर reply code `3`
- `0` -> Set: `SetCfmValue(key, value)` फिर reply code `1`
- `0x11` -> Unset: `UnSetCfmValue(key)` फिर reply code `0x12`
- `10` -> Commit: `SaveCfm2Flash()` फिर उत्तर `0x10` (ठीक) या `0xB` (त्रुटि) देता है
इसलिए `cfmd` का अनुकरण करने के लिए मैंने एक छोटी स्क्रिप्ट `cfm_stub` बनाई।```C
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#define SOCK_PATH "/var/cfm_socket"
// minimal UNIX-domain server that httpd expects.
// Replies with an IP string when it sees the key it asks for.
int main(void) {
int s = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = {0};
if (s < 0) { perror("socket"); return 1; }
unlink(SOCK_PATH);
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path)-1);
if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); return 1; }
if (listen(s, 5) < 0) { perror("listen"); return 1; }
for (;;) {
int c = accept(s, NULL, NULL);
if (c < 0) { if (errno==EINTR) continue; perror("accept"); break; }
char buf[1024]; ssize_t n = read(c, buf, sizeof(buf));
if (n > 0) {
// In some builds httpd asks for "lan.webiplansslen" etc.
// Any non-empty reply that looks like an IP keeps init happy.
const char *reply = "192.168.0.1";
write(c, reply, strlen(reply));
}
close(c);
}
close(s);
return 0;
}
अगले भाग के बाद मैं इसे संकलित करने का तरीका दिखाऊंगा।
अगली सहायक फ़ाइल hooks.so है। httpd और cfm में कुछ फ़ंक्शन हैं जो गैर-मौजूद हार्डवेयर के साथ बातचीत करने का प्रयास करते हैं।
हमारा प्रोग्राम शुरू होने पर निम्नलिखित मान लेता है।
/dev/nvram) मौजूद है और सामान्य डिफ़ॉल्ट मान लौटाता है।निम्नलिखित फ़ंक्शन एक समस्या बन जाते हैं और इसलिए हमें LD_PRELOAD=/hooks.so के साथ पैच करना होगा।
get_flash_type() -> यदि यह 4 लौटाता है, तो कोड फ़ाइल-आधारित पथ (cfm_file_init) लेता है, अन्यथा यह MTD से बात करने का प्रयास करता है (जो हमारे पास नहीं है)।get_cfm_blk_size_from_cache() (और/या वैरिएंट j_get_cfm_blk_size_from_cache) से परामर्श लिया जाता है।bcm_nvram_get) के लिए कॉल विफल नहीं होनी चाहिए अन्यथा स्टैक मानता है कि "NVRAM नष्ट हो गया" और रिस्टोर/रीबूट लॉजिक में चला जाता है।load_l7setting_file() और restore_power() जैसी रूटीन से सफल होने की उम्मीद है लेकिन वे गैर-मौजूद हार्डवेयर/फ़ाइलों को छूती हैं।यहाँ हमारा hooks.c है। मूल के लिए azeria-labs को श्रेय।```C
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>
int j_get_cfm_blk_size_from_cache(const int i) { puts("j_get_cfm_blk_size_from_cache called....\n"); return 0x20000; // 128 KiB block — what the file path expects }
int get_flash_type() { puts("get_flash_type called....\n"); return 4; // force file-backed CFM init, not MTD }
int load_l7setting_file() { puts("load_l7setting_file called....\n"); return 1; // pretend Layer-7 settings loaded OK }
int restore_power(int a, int b) { puts("restore_power called....\n"); return 0; // success (don’t touch RF/power hardware) }
char *bcm_nvram_get(char *key) { char *value = NULL;