여러 기능과 입력받은 URL을 확인하는 봇이 구현된 서비스입니다.
CSRF 취약점을 이용해 플래그를 획득하세요.
사용자가 자신의 의지와는 무관하게 공격자가 의도한 행위(수정, 삭제, 등록 등)를 특정 웹 사이트에 요청하게 하는 공격 유형
[vuln(csrf) 페이지]에서는 vuln 파라미터로 alert(1)를 실행하는 스크립트 구문을 전달한다.
응답 화면에서는 script 구문이 필터링된 것을 볼 수 있다.
<script>alert(1)</script> 가 <*>alert(1) 로 필터링 되었다.
@app.route("/vuln")
def vuln():
param = request.args.get("param", "").lower()
xss_filter = ["frame", "script", "on"]
for _ in xss_filter:
param = param.replace(_, "*")
return param
[vuln 페이지]는 vuln 파라미터로 전달된 값에서 frame, script, on 을 *로 치환하는 필터링이 존재한다.
vuln 페이지는 스크립트 구문(script, iframe, on) 필터링이 있는 페이지이다.
[memo 페이지]에 접속하면 memo 파라미터로 hello를 전달하고, 응답 화면에서는 hello를 출력한다.
여러번 접속해서 hello가 연속적으로 출력된다.
@app.route("/memo")
def memo():
global memo_text
text = request.args.get("memo", None)
if text:
memo_text += text
return render_template("memo.html", memo=memo_text)
[memo 페이지]는 memo 파라미터로 전달된 값에서 < 를 <
로 치환하는 필터링이 존재한다.
memo 페이지는 최종적으로 flag가 출력되는 페이지다.
[notice flag 페이지]에 접속하면 Acess Denied 문구가 출력된다.
@app.route("/admin/notice_flag")
def admin_notice_flag():
global memo_text
if request.remote_addr != "127.0.0.1":
return "Access Denied"
if request.args.get("userid", "") != "admin":
return "Access Denied 2"
memo_text += f"[Notice] flag is {FLAG}\n"
return "Ok"
[notice flag 페이지]는 접속 요청 사용자의 ip 주소가 127.0.0.1이 아니면 Access Denied 문자열을 출력하고
userid 파라미터 값이 admin이 아니면 Your not admin 문자열을 출력한다.
-> ip 주소가 127.0.0.1이고, userid가 admin이면 memo에 flag를 작성한다.
notice flag 페이지는 userid 값이 admin 이면 웹 서버 local 환경에서 flag를 memo로 추가하는 페이지다.
[flag]페이지에 접속하면 제출하는 버튼이 보인다. 아까 csrf 페이지와 경로가 같은 것으로 보인다.
csrf 페이지에서 보았던 alert(1)를 출력하는 구문을 보내보자.
good 이라는 메세지가 출력되는게 끝이다.
@app.route("/flag", methods=["GET", "POST"])
def flag():
if request.method == "GET":
return render_template("flag.html")
elif request.method == "POST":
param = request.form.get("param", "")
if not check_csrf(param):
return '<script>alert("wrong??");history.go(-1);</script>'
return '<script>alert("good");history.go(-1);</script>'
[flag 페이지]에서는 param 파라미터 값을 전달하고
check_csrf(param) 함수가 정상 동작한다면 good 문자열을 출력하고 check_csrf(param) 함수가 정상 동작하지 않으면, wrong 문자열을 출력한다.
flag 페이지는 csrf 페이지를 웹 서버 local IP로 접속하는 페이지이다.
def read_url(url, cookie={"name": "name", "value": "value"}):
cookie.update({"domain": "127.0.0.1"})
try:
options = webdriver.ChromeOptions()
for _ in [
"headless",
"window-size=1920x1080",
"disable-gpu",
"no-sandbox",
"disable-dev-shm-usage",
]:
options.add_argument(_)
driver = webdriver.Chrome("/chromedriver", options=options)
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
driver.get("http://127.0.0.1:8000/")
driver.add_cookie(cookie)
driver.get(url)
except Exception as e:
driver.quit()
print(str(e))
# return str(e)
return False
driver.quit()
return True
read_url(url, cookie={"name": "name", "value": "value"})
read_url() 함수는 url과 cookie를 매개변수로 갖는 함수이다.
함수가 호출되면 cookie 변수 값이 "domain": "127.0.0.1"로 업데이트 된다.
그리고 driver 객체가 선언된다.
driver: 파이썬의 selenium 라이브러리를 이용한 크롬 웹 브라우저 제어(webdriver) 객체가 저장된 변수
driver는 http://127.0.0.1:8000/ 페이지에 접속하고, cookie에 저장된 값을 cookie로 추가한다.
def check_csrf(param, cookie={"name": "name", "value": "value"}):
url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
return read_url(url, cookie)
그리고 driver는 http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)} 에 접속한다.
urllib.parse.quote(url): 아스키 형식이 아닌 문자열을 url 인코딩 하는 함수이다.
flag 페이지에 csrf 파라미터 값을 입력하고 제출 버튼을 누르면 서버에서는 아래 url에 접속한다.
http://127.0.0.1:8000/csrf?csrf={사용자가 입력한 csrf 파라미터 값}
여기에서 csrf 파라미터 값만 조작할 수 있기 때문에 이 값으로 notice flag 페이지를 접속하도록 유도해야한다.
script, iframe, on 문자열이 치환되어 있지만, 단순 웹 페이지에 접속하는 구문만 실행시키려면
img 태그의 src 속성을 이용할 수 있다. 코드를 작성해보자.
<img src=/admin/notice_flag?userid=admin>
위 img 태그는 /admin/notice_flag?userid=admin 경로에서 이미지를 불러오는 태그인데
실제 이미지는 아니지만 일단 이미지를 불러오려는 시도를 하기 때문에 페이지에 접속하게 된다.
이 때 공격 구문이 성공할 수 있다. 진행시켜보자.
참고
https://mokpo.tistory.com/98
위 티스토리 토대로 따라해보았다..어렵다....