[Study] ALLES! CTF 2021 J(ust)-S(erving)-P(ages) Writeup
주어진 URL에 접근해보자. 눈에 보여지는 기능은 로그인/로그아웃, 회원가입 기능 뿐이었다. 아마 admin 권한 계정으로 접근하면 플래그를 출력해주는 것 같다.
주어진 소스를 분석해보자.
*ConfigSevlet.java
package cscg.servlets;
import java.io.*;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.fasterxml.jackson.databind.ObjectMapper;
import cscg.user.UserConfig;
@WebServlet("/config")
public class ConfigServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public ConfigServlet() {
super();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher("config.jsp");
request.setAttribute("message", "Invalid request");
request.setAttribute("type", "danger");
dispatcher.forward(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
RequestDispatcher dispatcher = request.getRequestDispatcher("config.jsp");
String jsonConfig = getBody(request);
if (jsonConfig == null) {
request.setAttribute("message", "Invalid request");
request.setAttribute("type", "danger");
dispatcher.forward(request, response);
return;
}
HttpSession session = request.getSession(true);
ObjectMapper objectMapper = new ObjectMapper();
UserConfig userConfig = objectMapper.readValue(jsonConfig, UserConfig.class);
##### ..................................................................... (1)
if (userConfig == null) {
request.setAttribute("message", "Failed to parse user configuration");
request.setAttribute("type", "danger");
}
else if (userConfig.getUser() != null) { ##### ............................ (2)
request.setAttribute("type", "danger");
request.setAttribute("message", "Hacking detected!");
}
else {
request.setAttribute("type", "success");
request.setAttribute("message", "User configuration updated");
session.setAttribute("config", userConfig); ##### ..................... (3)
}
dispatcher.forward(request, response);
}
public static String getBody(HttpServletRequest request) throws IOException {
String body = null;
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
return null;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
return null;
}
}
}
body = stringBuilder.toString();
return body;
}
}
분석할만한 추가 기능은 /config 밖에 없었는데, 위와 같이 작동한다. POST로 json data를 입력받아 config 적용을 시키는 것이다.
조건은, (1) 과 같이 UserConfig.class 형식을 따라야 하며, (2) 와 같이 getUser()의 값이 Null이어야 한다.
두 조건을 만족시키면 (3) 과 같이 입력한 Config가 적용이 된다. 입력 양식을 맞추기 위해 UserConfig.java 를 분석해보았다.
*UserConfig.java
package cscg.user;
public class UserConfig {
private boolean debugMode;
private int language;
private User user;
public UserConfig() {
this.debugMode = false;
this.language = 0;
this.user = null;
}
public boolean isDebugMode() {
return debugMode;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public int getLanguage() {
return language;
}
public void setLanguage(int language) {
this.language = language;
}
public void setDebugMode(boolean debugMode) {
this.debugMode = debugMode;
}
}
조건 중 getUser의 값이 Null이어야만 했으므로, 결국 아래 처럼 입력되어야 할 것이다.
{
"debugMode":1,
"language";1
}
이제 debugMode와 language가 어떤 역할을 하는지 분석할 차례이다. language같은 경우 큰 의미 없었고 debugMode는 아래 코드 내용에 참조되었다.
*UserDAO.java
package cscg.user;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.*;
import javax.servlet.http.HttpServletRequest;
public class UserDAO {
public User checkLogin(HttpServletRequest request, String username, String password_md5_sha1) {
User user = null;
ArrayList<User> users = (ArrayList<User>) request.getServletContext().getAttribute("users");
if (users == null)
return null;
for (User u : users) {
if (u.getUsername().equals(username)) {
try {
// User password in storage is only stored as md5, we should hash it again
MessageDigest digestStorage;
digestStorage = MessageDigest.getInstance("SHA-1"); ##### .................. (1)
digestStorage.update(u.getPassword().getBytes("ascii"));
byte[] passwordBytes = null;
try {
passwordBytes = Hex.decodeHex(password_md5_sha1);
} catch (DecoderException e) {
return null;
}
UserConfig userConfig = (UserConfig) request.getSession().getAttribute("config");
if (userConfig.isDebugMode()) {
String pw1 = new String(Hex.encodeHex(digestStorage.digest())); #### ... (2)
String pw2 = password_md5_sha1;
java.util.logging.Logger.getLogger("login")
.info(String.format("Login tried with: %s == %s", pw1, pw2));
}
if (Arrays.equals(passwordBytes, digestStorage.digest())) { ##### .......... (3)
if (userConfig.isDebugMode())
java.util.logging.Logger.getLogger("login").info("Passwords were equal");
return u;
}
if (userConfig.isDebugMode())
java.util.logging.Logger.getLogger("login").info("Passwords were NOT equal");
} catch (NoSuchAlgorithmException e) {
return null;
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
return null;
}
}
debugMode가 활성화 되면, 일반적인 경우와 다르게 (2),(3)에서 총 2번의 digest()가 일어난다. digest()의 기능은 (1)과 같이 sha-1 처리를 해주는 것이다. 실제로, 계정 생성 후 debugMode가 활성화되면 입력했던 패스워드로 로그인이 되지 않는다.
앞서 언급했듯, sha-1 처리를 해주는 digest()가 두 번 일어나서 sha-1이 두번 되어서 그런것이라고 생각했다.
* 참고로, 회원 가입시 패스워드를 입력하면, MD5 처리가 된 후 서버에 저장된다. 로그인 검증을 할 때는 MD5된 password를 불러와서 digest(sha-1)을 하고, 패스워드 입력 시 md5->sha-1 처리되어 온 값과 비교한다.
*main.js
function pwd_handler_login(form)
{
if (form.password.value != '')
{
form.password.value = CryptoJS.MD5(form.password.value).toString();
form.password.value = CryptoJS.SHA1(form.password.value).toString();
console.log(form.password.value);
}
}
function pwd_handler_registration(form)
{
if (form.password.value != '')
{
form.password.value = CryptoJS.MD5(form.password.value).toString();
console.log(form.password.value);
}
}
다시 본론으로 돌아와서,
내가 분석한 내용이 맞다면 debugMode 활성 화 후, 전달되는 값에서 sha-1을 한번 더 한 값으로 로그인을 시도하면 성공해야 할 것이다.
하지만, 한 번 더 sha-1 처리한 값으로는 로그인이 되지 않았다...
원인을 파악하기 위해 해당 부분이 실제로 어떻게 동작하는지 확인하기 위해 로컬에서 테스트를 해보았다.
first sha-1 : 4028a0e356acc947fcd2bfbf00cef11e128d484a
second sha-1 : da39a3ee5e6b4b0d3255bfef95601890afd80709 (?)
실제 테스트 결과 예상했던 것과 결과가 달랐다. 구글링을 해보니 두 번째 값은 빈 문자열의 해쉬 값이라고 한다.
https://ko.wikipedia.org/wiki/SHA
SHA - 위키백과, 우리 모두의 백과사전
SHA(Secure Hash Algorithm, 안전한 해시 알고리즘) 함수들은 서로 관련된 암호학적 해시 함수들의 모음이다. 이들 함수는 미국 국가안보국(NSA)이 1993년에 처음으로 설계했으며 미국 국가 표준으로 지정
ko.wikipedia.org
결과적으로, debugMode를 활성화 하면 md5된 password에 sha-1 처리를 두 번 하는줄 알았으나 어떠한 이유로 빈 문자열의 해쉬값이 된다. 즉, 패스워드가 da39~~ 로 통일된다는 뜻이다.
실제로 입력값이 md5된 test 문자열 일때나 AnyString.. 이라는 문자열 일때나 최종 결과값이 같음을 확인 할 수 있었다.
test(MD5)'s first sha-1 : 4028a0e356acc947fcd2bfbf00cef11e128d484a
test(MD5)'s second sha-1 : da39a3ee5e6b4b0d3255bfef95601890afd80709
AnyString's first sha-1 : dde5b0107bda0ffa980d02c573f8eff1480d6cb4
AnyString' second sha-1 : da39a3ee5e6b4b0d3255bfef95601890afd80709
테스트 코드는 아래와 같다.
import java.security.MessageDigest;
/**
* test
*/
public class test {
public static void main(String[] args) {
try {
MessageDigest digestStorage;
digestStorage = MessageDigest.getInstance("SHA-1");
digestStorage.update("098f6bcd4621d373cade4e832627b4f6".getBytes());
byte[] result1 = digestStorage.digest();
StringBuffer sb1 = new StringBuffer();
for (int i = 0; i < result1.length; i++) {
sb1.append(Integer.toString((result1[i] & 0xff) + 0x100, 16).substring(1));
}
System.out.println("test(MD5)'s first sha-1 : " + sb1.toString());
byte[] result2 = digestStorage.digest();
StringBuffer sb2 = new StringBuffer();
for (int i = 0; i < result2.length; i++) {
sb2.append(Integer.toString((result2[i] & 0xff) + 0x100, 16).substring(1));
}
System.out.println("test(MD5)'s second sha-1 : " + sb2.toString());
MessageDigest digestStorage2;
digestStorage2 = MessageDigest.getInstance("SHA-1");
digestStorage2.update("AnyString..".getBytes());
byte[] result3 = digestStorage2.digest();
StringBuffer sb3 = new StringBuffer();
for (int i = 0; i < result3.length; i++) {
sb3.append(Integer.toString((result3[i] & 0xff) + 0x100, 16).substring(1));
}
System.out.println("AnyString's first sha-1 : " + sb3.toString());
byte[] result4 = digestStorage2.digest();
StringBuffer sb4 = new StringBuffer();
for (int i = 0; i < result4.length; i++) {
sb4.append(Integer.toString((result4[i] & 0xff) + 0x100, 16).substring(1));
}
System.out.println("AnyString' second sha-1 : " + sb4.toString());
} catch (Exception e) {
return;
//TODO: handle exception
}
}
}
따라서, debugMode를 활성화시키면 모든 계정의 패스워드가
da39a3ee5e6b4b0d3255bfef95601890afd80709
로 통일된다는 의미이다. admin계정으로 로그인을 시도하면서 전달되는 패스워드를 위 값으로 전송하면 로그인 성공하여 플래그를 획득 할 수 있다.
해당 로직이 취약했던 원인은 digest()는 한 번 호출되면 초기값으로 초기화되는 성질이 있기 때문이다. 문제 로직에서는 최초에 update를 한 번만한 상태에서 digest()를 두 번 사용하여, 첫 번째 digest() 이후 빈문자열로 초기화된 상태에서 digest()를 했던 것이 취약점이었다.
https://docs.oracle.com/javase/7/docs/api/java/security/MessageDigest.html
MessageDigest (Java Platform SE 7 )
This MessageDigest class provides applications the functionality of a message digest algorithm, such as SHA-1 or SHA-256. Message digests are secure one-way hash functions that take arbitrary-sized data and output a fixed-length hash value. A MessageDigest
docs.oracle.com
FLAG = ALLES!{ohh-b0y-java-y-u-do-th1s-t0-m3???!?}
----------------------------------------------------------------------------------------------------------------------
결과적으로 삽질이었지만, 문제풀이를 시도하면서 공부한 내용들을 간단하게 메모해놓자.
(추후에 쓰일 일이 있을지도 모른다고 최대한 의미부여해보자..)
로직에서 취약점을 못찾고 tomcat 버전(8.5.43) 자체의 취약점인가 해서 그쪽으로 삽질을 많이했다.
1. 해당 버전에서 .session파일 업로드 가능할 때 RCE 취약점(CVE-2020-9484)
https://m.blog.naver.com/skinfosec2000/222042818637
[Research & Technique] Apache Tomcat Session Deserialization (CVE-2020-9484) 취약점
■ 취약점 개요 2020년 5월 20일, Apache Tomcat에서 신규 취약점(CVE-2020-9484)이 공개되었다. CV...
blog.naver.com
2. 해당 버전에서 AJP 활성화 되어있을시 LFI 취약점(CVE-2020-1938)
Ghostcat : Tomcat-Ajp 프로토콜 취약점 (cve-2020-1938) 주의!
Apache Tomcat 서버에 존재하는 파일에 취약점이 포함되어 있어, 공격자가 해당 취약점을 악용하여 Tomcat의 webapp목록 하위에 있는 모든 임의의 파일을 읽어들일수 있습니다. 또한 취약한 서버가 파
blog.alyac.co.kr
3. 특정 버전의 fasterxml에서의 RCE 취약점(CVE-2020-35728)
해당 문제에서의 버전에서는 해당되지 않았다.
https://github.com/Al1ex/CVE-2020-35728
GitHub - Al1ex/CVE-2020-35728: CVE-2020-35728 & Jackson-databind RCE
CVE-2020-35728 & Jackson-databind RCE. Contribute to Al1ex/CVE-2020-35728 development by creating an account on GitHub.
github.com
4. JSON Deserialization RCE 취약점(CVE-2019-12384, 3번과 동일한 내용인듯)
JSON Deserialization Vulnerability - HackTheBox Time
Premise In this video walkthrough, we covered a vulnerability in Jackson library that uses JSON Deserialization and used ‘Time‘ machine from Hackthebox for demo purposes....
motasem-notes.net
5. setattribute()에서 활용 가능한 EL Injection RCE 취약점