[XCTF]华为第二场
这个星期好像华为三连。。。今天这场选星期三刚好完全满课,下午就没机会看了,中午花了一个小时做了一个web签到呜呜,估计下午肯定还有一堆新题
babyphp
给了一个莫名其妙的扫描扫描器界面,可选ip端口和超时时间,随便怎么输都是回显Port scan is deperacted and try to find the source code! // Google is your best friend
让人不知道想表达什么,意思是要谷歌出源码?
我反正是没懂什么情况,然后无敌老国王直接从GitHub给我丢了一份源码
直接把HTML复制粘贴在GitHub上一搜就能搜到对应的代码
太牛逼了
<?php
set_time_limit(0);//设置程序执行时间
ob_implicit_flush(True);
ob_end_flush();
$url = isset($_REQUEST['url'])?$_REQUEST['url']:null;
/*端口扫描代码*/
.....这段就是被出题人魔改去掉的部分
/*内网代理代码*/
function getHtmlContext($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, TRUE); //表示需要response header
curl_setopt($ch, CURLOPT_NOBODY, FALSE); //表示需要response body
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
$result = curl_exec($ch);
global $header;
if($result){
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = explode("\r\n",substr($result, 0, $headerSize));
$body = substr($result, $headerSize);
}
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '200') {
return $body;
}
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) == '302') {
$location = getHeader("Location");
if(strpos(getHeader("Location"),'http://') == false){
$location = getHost($url).$location;
}
return getHtmlContext($location);
}
return NULL;
}
function getHost($url){
preg_match("/^(http:\/\/)?([^\/]+)/i",$url, $matches);
return $matches[0];
}
function getCss($host,$html){
preg_match_all("/<link[\s\S]*?href=['\"](.*?[.]css.*?)[\"'][\s\S]*?>/i",$html, $matches);
foreach($matches[1] as $v){
$cssurl = $v;
if(strpos($v,'http://') == false){
$cssurl = $host."/".$v;
}
$csshtml = "<style>".file_get_contents($cssurl)."</style>";
$html .= $csshtml;
}
return $html;
}
if($url != null){
$host = getHost($url);
echo getCss($host,getHtmlContext($url));
}
?>
getHtmlContext就是一个能追踪重定向的函数(但是实际使用的时候发现好像不是很能追踪。。。vps上设置了http跳转https,然后好像没追踪过去,我感觉是重定向要求是http那里出问题了)
getHost就获取域名,限制了http协议
getCss,不知道为什么扫描器要抓css。。。但是这是一个ssrf的点,preg_match_all会匹配子正则表达式,就是小括号里的正则表达式
matches[0]存整个正则表达式匹配到的项,matches[1]及往后的项保存子组的匹配
所以这里的matches[1]就是所以满足(.*?[.]css.*?)
这么个正则表达式的匹配项
然后检测一下matches[1]里面有没有http:// 这个字符串,没得就把前面拼一个host,最后直接进行一个file_get_contents
其实蛮简单的,绕过.css和http:// 这两个限制就行了,经典Linux跳目录之术
在自己vps上放一个符合getCss正则的消息就行,href填如下消息即可.css/http://../../../../../var/www/html/index.php
读根目录flag没读到,然后试了一波flag.php,读到flag
云存储
晚上再来看题,一共就两个web,后面出的这个是nodejs的题
给了源码,有上传下载功能,感觉毫无用处,和ssrf功能,本地访问flag路由时返回flag
用的req.ip,理论上和PHP的remote_address应该是一个东西
ssrf功能就是给个链接就去访问,但是需要过一个超级check
const check = function(s) {
if (!typeof (s) == 'string' || !s.match(/^http\:\/\//))
return false
let blacklist = ['wrong', '127.', 'local', '@', 'flag']
let host, port, dns;
host = url.parse(s).hostname
port = url.parse(s).port
if ( host == null || port == null)
return false
dns = dnslookup(host);
if ( ip.isPrivate(dns) || dns != docker.ip || ['80','8080'].includes(port) )
return false
for (let i = 0; i < blacklist.length; i++)
{
let regex = new RegExp(blacklist[i], 'i');
try {
if (ip.fromLong(s.replace(/[^\d]/g,'').substr(0,10)).match(regex))
return false
} catch (e) {}
if (s.match(regex))
return false
}
return true
}
dnslookup函数就是去访问一个特定接口查询域名对应的ip,然后需要dns查询出的结果不是私有地址,且dns查询ip等于本机公网ip,端口不是80和8080
最后过一个正则,提交链接中不能有黑名单中的字符
DNS rebinding
学到的新操作
DNS重定向,用于ssrf的waf绕过,也能bypass同源策略
waf绕过
就本题中,进行DNS查询时,除了返回域名对应的ip,还会返回一个TTL,表明这条记录的有效时间,如果我们在check时域名对应的ip为符合要求的公网ip,而check结束后请求的ip却变回内网ip,就能进行绕过
我们令TTL=0,查询结果就变成一次性的,即可在check时得到的DNS记录在真实发送时无效,再查一次结果获得的是127.0.0.1完成利用
同源策略bypass
看bendawang师傅的博客
关于DNS-rebinding的总结
思路就是钓鱼网站为www.xxx.com ,其中有一个对www.xxx.com/source 的请求,再把这个请求转发出来。当用户访问www.xxx.com 时,查询DNS获取一个正常的公网地址,而将TTL设为很短的时间,然后整一个setTimeout,在一段时间后请求www.xxx.com/source 的内容,但是此时DNS返回的www.xxx.com 的ip地址却是127.0.0.1,这样子就能将用户本地source下的内容转发出来
但是浏览器觉得这只是www.xxx.com 在操作自身域的资源,因此允许进行转发,并未违背同源策略,完成利用
DNS rebinding实现
1.设置两个域名相同的A记录,每次查询随机返回一个,撞大运(没有实践过,且国内域名TTL最短为十分钟,感觉需要奇怪的国外服务TTL可以调到0才有机会)
2.在线工具,可以实现DNS重绑定,不过也是撞大运 https://lock.cmpxchg8b.com/rebinder.html
3.自建DNS服务器,具体内容也是参考的bendawang师傅的博客
端口绕过
url.parse(s).port在不填端口号的时候解析出来是null
include比较是字符串比较,端口号用0080绕过即可
路由绕过
不会,感觉express路由不支持编码,Unicode url编码都试过了都不行,fuzz特殊字符的话直接爆炸
app.py里嵌套了两层中间件,但是用起来的感觉是只有一层,不知道是不是有什么trick
app.use(express.urlencoded({extended: false}));
app.use(express.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
二次编码的结果是只解码了一次,request.get发出去的请求不会自己解码,路由收到url编码也不会解码,过不去呜呜
等wp了
update
问了下rmb神仙,他又把这个题秒了
request.get跟踪302,和python的requests.get差不多,总觉得以前好像做过python重定向的题,这次居然没想到,呜呜
只需要第一次查询ip解析到docker本身,第二次访问解析到我的vps,再在我的vps上放一个重定向就可以了,可以另开端口启动服务,也就不需要端口绕过了
重定向一开始我傻不拉几的用js实现,写一个windows.location….
后来发现并不对,js抓取这个内容并不能解析这个重定向,以前用这个重定向打通了好像是因为后台有一个bot用无头浏览器,可以模仿浏览器行为就重定向成功了
所以应该直接在请求头里面重定向,<?php Header("Location:http://127.0.0.1/flag" ); ?>
我太垃圾了呜呜
REMOTE_ADDR
其实是一个非常愚蠢的问题。。。
昨天我一直在想为什么有ip.isPrivate(dns) || dns != docker.ip
这么一个过滤要求dns查询的ip结果不是127.0.0.1且得等于自己的公网ip,我就很困惑。。既然已经是自己访问自己了,为什么要ban掉127.0.0.1却允许从公网访问自己,难道从公网自己访问自己REMOTE_ADDR就不是127.0.0.1吗
答案还真是就会变,所以虽然docker是把80端口映射到了8010端口,但是通过公网ip加8010端口的访问的REMOTE_ADDR就是公网ip,所以需要本地访问80端口才能打通,呜呜,我太垃圾了
因为请求非自身的ip肯定是要去问路由器的,就算是自己访问自己也需要跳到路由器上再跳回来,这样子访问的远端ip就变成了自己的公网ip(内网也一样),而不是127.0.0.1
因此想remote_addr是127.0.0.1,就必须是直接显式的访问127.0.0.1,这样子才不会去走路由,直接访问自己
自建DNS
注册到systemctl,快速重启,稳定不需要撞大运(虽然话是这么说,但是用起来感觉好像还是是没有想象中百发百中的稳定性)
写一个mydns.service放到/etc/systemd/system目录下,这个是Ubuntu的路径,网上找的大部分usr目录下的那个是centOS的
[Unit]
Description=mydns
[Service]
Type=simple
ExecStart=/root/dns_rebinding/dns_rebinding.py
ExecReload=/root/dns_rebinding/dns_rebinding.py
ExecStop=/bin/kill -s TERM $MAINPID
[Install]
WantedBy=multi-user.targetroot
写一个简单的服务功能就行,定义start restart 和 stop都执行什么命令就行
dns_rebinding.py就直接抄bendawang师傅博客里的脚本就行,python2运行
加内联注释#!/usr/bin/env python2
改成755可执行
systemctl enable mydns.service
systemctl start mydns.service即可