AFL++实战其五

练习5 - LibXML2

漏洞编号:CVE-2017-9048

CVE-2017-9048 是一个影响 LibXML2 DTD 验证功能的栈缓冲溢出漏洞。

栈缓冲区溢出是一种缓冲区溢出类型,其中被覆盖的缓冲区被分配到栈中。

因此,远程攻击者可以利用此问题在使用受影响库的应用程序上下文中执行任意代码。


下载LibXML2并构建

1
2
wget https://github.com/GNOME/libxml2/archive/refs/tags/v2.9.4.tar.gz
x ./v2.9.4.tar.gz # x是oh_my_zsh的解压插件,会自适应解压不同类型压缩包
1
CC=afl-clang-lto CXX=afl-clang-lto++ CFLAGS="-fsanitize=address" CXXFLAGS="-fsanitize=address" LDFLAGS="-fsanitize=address" ./configure --prefix="$HOME/fuzz_main/install/" --disable-shared --without-debug --without-ftp --without-http --without-legacy --without-python LIBS='-ldl'

AFL_USE_ASAN=1 已经被包含在CFLAGS="-fsanitize=address" CXXFLAGS="-fsanitize=address" LDFLAGS="-fsanitize=address"中。

这些without-*参数可查阅 github 的 LibXML 文档得知其意义。

种子语料库获取

1
2
3
4
5
cd $HOME/fuzz_main/fuzz/
mkdir seeds
cd seeds
wget https://raw.githubusercontent.com/antonio-morales/Fuzzing101/main/Exercise%205/SampleInput.xml
cd ..

克隆字典:

1
2
3
4
5
6
cd $HOME
cd sysset
mkdir fuzz_dictionaries
cd fuzz_dictionaries
mkdir dictionaries && cd dictionaries
wget https://raw.githubusercontent.com/AFLplusplus/AFLplusplus/stable/dictionaries/xml.dict

开始模糊

这次我们采用双实例模糊(在多实例模糊下使用的种子需要不同):

主:

1
afl-fuzz -m none -i $HOME/fuzz_main/fuzz/seeds/ -o ./out -s 123 -x $HOME/sysset/fuzz_dictionaries/dictionaries/xml.dict -D -M master -- $HOME/fuzz_main/install/bin/xmllint --memory --noenc --nocdata --dtdattr --loaddtd --valid --xinclude @@

这里是使用了-D参数,此时使用确定性变异算法。该算法会采用位翻转、字节翻转、算术运算、已知整数插入、字典替换来实现输入变异。这样比随机变异更慢但是覆盖率更高。只有主实例才能使用-D参数。

从:

1
afl-fuzz -m none -i $HOME/fuzz_main/fuzz/seeds/ -o ./out -s 234 -S slave1 -- $HOME/fuzz_main/install/bin/xmllint --memory --noenc --nocdata --dtdattr --loaddtd --valid --xinclude @@

你需要两个窗口来运行这些命令。

如果成功,你会在右下角看见一个是 CPU000 另外一个是 CPU001。

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
   AFL ++4.41a {master} (/home/zlsf/fuzz_main/install/bin/xmllint) [explore]
┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│ run time : 0 days, 7 hrs, 17 min, 34 sec │ cycles done : 14 │
│ last new find : 0 days, 0 hrs, 0 min, 35 sec │ corpus count : 6793 │
│last saved crash : none seen yet │saved crashes : 0 │
│ last saved hang : none seen yet │ saved hangs : 0 │
├─ cycle progress ─────────────────────┬─ map coverage┴──────────────────────┤
│ now processing : 1*1 (0.0%) │ map density : 1.54% / 5.61% │
│ runs timed out : 0 (0.00%) │ count coverage : 4.66 bits/tuple │
├─ stage progress ─────────────────────┼─ findings in depth ─────────────────┤
│ now trying : havoc │ favored items : 100 (1.47%) │
│ stage execs : 56/150 (37.33%) │ new edges on : 26 (0.38%) │
│ total execs : 7.47M │ total crashes : 0 (0 saved) │
│ exec speed : 305.4/sec │ total tmouts : 76 (0 saved) │
├─ fuzzing strategy yields ────────────┴─────────────┬─ item geometry ───────┤
│ bit flips : 0/112k, 0/112k, 0/112k │ levels : 18 │
│ byte flips : 0/14.0k, 0/14.0k, 0/14.0k │ pending : 3518 │
│ arithmetics : 0/982k, 0/1.96M, 0/1.96M │ pend fav : 0 │
│ known ints : 0/126k, 0/533k, 0/785k │ own finds : 4985 │
│ dictionary : 0/4.11M, 0/4.11M, 0/0, 0/0 │ imported : 1807 │
│havoc/splice : 223/659k, 0/0 │ stability : 100.00% │
│py/custom/rq : unused, unused, unused, unused ├───────────────────────┘
│ trim/eff : disabled, 99.99% │ [cpu000:125%]
└─ strategy: explore ────────── state: in progress ──┘^C

+++ Testing aborted by user +++
[*] Writing ./out/master/fastresume.bin ...
[+] fastresume.bin successfully written with 2755089 bytes.
[+] We're done here. Have a nice day!

主播这里跑了七个小时,四个实例,无结果。


这里我们就直接使用 Fuzzing101 的结果图就寻找源码漏洞吧,毕竟我们的任务是学习 FUZZ,而不是去重复去挖掘老漏洞像当新漏洞一样重新浪费时间挖一遍。

不过模糊测试挖掘漏洞时一般时长在 48 小时以内应该都是可接受的。


源码阅读

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
void
xmlSnprintfElementContent(char *buf, int size, xmlElementContentPtr content, int englob) {
int len;

if (content == NULL) return;
len = strlen(buf);
if (size - len < 50) {
if ((size - len > 4) && (buf[len - 1] != '.'))
strcat(buf, " ...");
return;
}
if (englob) strcat(buf, "(");
switch (content->type) {
case XML_ELEMENT_CONTENT_PCDATA:
strcat(buf, "#PCDATA");
break;
case XML_ELEMENT_CONTENT_ELEMENT:
if (content->prefix != NULL) {
if (size - len < xmlStrlen(content->prefix) + 10) {
strcat(buf, " ...");
return;
}
strcat(buf, (char *) content->prefix);
strcat(buf, ":");
}
if (size - len < xmlStrlen(content->name) + 10) { // xmlStrlen函数才是元凶
strcat(buf, " ...");
return;
}
if (content->name != NULL)
strcat(buf, (char *) content->name); // 漏洞在这
break;
// ...

该函数的调用是xmlSnprintfElementContent(&expr[0], 5000, cont, 1);其中expr[0]的长度就是 5000 。

1
2
3
4
5
6
7
8
9
10
11
int
xmlStrlen(const xmlChar *str) {
int len = 0;

if (str == NULL) return(0);
while (*str != 0) { /* non input consuming */
str++;
len++;
}
return(len);
}

这看起来是作者自己实现的字符串长度获取,但是记录长度的变量类型是 int ,这也就意味着会产生溢出。

修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int
xmlStrlen(const xmlChar *str) {
int len = 0;

if (str == NULL) return(0);
while (*str != 0) { /* non input consuming */
if(len == 0x7FFFFFFF) {
printf("too long xml!");
exit(-1);
}
str++;
len++;
}
return(len);
}

临时修补改成这样即可。


AFL++实战其五
https://zlsf-zl.github.io/2026/04/11/AFL-实战其五/
作者
ZLSF
发布于
2026年4月11日
许可协议