概述
这是 WebGoat 的最后一部分,主要内容是 WebGoat中的Challenge,前面还有 1 和 2。
Challenge
Admin lost password
本题目的服务端源代码。
@AssignmentPath("/challenge/1")
public class Assignment1 extends AssignmentEndpoint {
@RequestMapping(method = RequestMethod.POST)
public
@ResponseBody
AttackResult completed(@RequestParam String username, @RequestParam String password, HttpServletRequest request) throws IOException {
boolean ipAddressKnown = true;
boolean passwordCorrect = "admin".equals(username) && PASSWORD.equals(password);
if (passwordCorrect && ipAddressKnown) {
return success().feedback("challenge.solved").feedbackArgs(Flag.FLAGS.get(1)).build();
} else if (passwordCorrect) {
return failed().feedback("ip.address.unknown").build();
}
return failed().build();
}
public static boolean containsHeader(HttpServletRequest request) {
return StringUtils.hasText(request.getHeader("X-Forwarded-For"));
}
}
public interface SolutionConstants {
//TODO should be random generated when starting the server
String PASSWORD = "!!webgoat_admin_1234!!";
String PASSWORD_TOM = "thisisasecretfortomonly";
String ADMIN_PASSWORD_LINK = "375afe1104f4a487a73823c50a9292a2";
}
可以看到是直接用两个输入和两个字符串常量做匹配,然后做了一个与操作。感觉无法绕过???
Without password
此题目要求用账户Larry登录,这道题目是一道万能密码题目。
使用Larry/1‘ or ‘1‘=1进行登录就可以了,看一下题目源代码
@RequestMapping(method = POST)
@ResponseBody
public AttackResult login(@RequestParam String username_login, @RequestParam String password_login) throws Exception {
Connection connection = DatabaseUtilities.getConnection(webSession);
checkDatabase(connection);
if (!StringUtils.hasText(username_login) || !StringUtils.hasText(password_login)) {
return failed().feedback("required4").build();
}
if (!"Larry".equals(username_login)) {
return failed().feedback("user.not.larry").feedbackArgs(username_login).build();
}
PreparedStatement statement = connection.prepareStatement("select password from " + USERS_TABLE_NAME + " where userid = ‘" + username_login + "‘ and password = ‘" + password_login + "‘");
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
return success().feedback("challenge.solved").feedbackArgs(Flag.FLAGS.get(5)).build();
} else {
return failed().feedback("challenge.close").build();
}
}
可以看到题目直接将接收到的用户名和密码代入了sql语句,所以使用万能密码后的拼好的完整sql语句为
select password from USERS_TABLE_NAME where userid = ‘Larry‘ and password = ‘1‘ or ‘1‘=‘1‘
此语句永远为为真,就登录了。
Creating a new account
在注册界面抓包后发现链接为 WebGoat/challenge/6
,查看源文件
@PutMapping //assignment path is bounded to class so we use different http method :-)
@ResponseBody
public AttackResult registerNewUser(@RequestParam String username_reg, @RequestParam String email_reg, @RequestParam String password_reg) throws Exception {
AttackResult attackResult = checkArguments(username_reg, email_reg, password_reg);
if (attackResult == null) {
Connection connection = DatabaseUtilities.getConnection(webSession);
checkDatabase(connection);
String checkUserQuery = "select userid from " + USERS_TABLE_NAME + " where userid = ‘" + username_reg + "‘";
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(checkUserQuery);
if (resultSet.next()) {
attackResult = failed().feedback("user.exists").feedbackArgs(username_reg).build();
} else {
PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO " + USERS_TABLE_NAME + " VALUES (?, ?, ?)");
preparedStatement.setString(1, username_reg);
preparedStatement.setString(2, email_reg);
preparedStatement.setString(3, password_reg);
preparedStatement.execute();
attackResult = success().feedback("user.created").feedbackArgs(username_reg).build();
}
}
return attackResult;
}
发现在String checkUserQuery = "select userid from " + USERS_TABLE_NAME + " where userid = ‘" + username_reg + "‘";
中直接把userid
插入了查询用户是否存在语句,则直接用sqlmap进行注入测试。测试包如下。
PUT /WebGoat/challenge/6 HTTP/1.1
Host: 127.0.0.1:8080
Content-Length: 84
Accept: */*
Origin: http://127.0.0.1:8080
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://127.0.0.1:8080/WebGoat/start.mvc
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7
Cookie: JSESSIONID=A7D0A7096B5E9685DF152BC343A54B8E
Connection: close
username_reg=tom*&email_reg=Tom%40Tom.com&password_reg=tom&confirm_password_reg=tom1
可以看见,登录过程和注册信息入库过程都启用了预编译,几乎没有什么注入可能,唯一有注入点的地方就是检查用户是否注册过这里,直接把username_reg拼接在sql语句中,我的用户名是breeze,再次注册会提示我已经注册过,但我如果把用户名改为breeze’ and1=2 –就会提示我创建账户成功。这样我们就可以在and后构造逻辑语句来进行布尔注入了。但问题是,如何知道表名。
在源码中,我们看出,这张表每次使用都会创建新的,用完删除,而表名是challenge_users_6加上随机生成的16位长度的字符串,几乎不可能暴力破解了。但它讲表名输出到了服务器的log上,所以我们可以去log查看本次的表名
这次的表名是challenge_users_6WDzKXNcjaYiNPkSr,根据这个表名构造逻辑语句,前面的用户我们使用没有注册过的breeze123,那么查询结果就是假,后面使用or+逻辑语句,这样我们的逻辑语句是真就会返回假,是假就会返回真。
逻辑语句:
breeze123‘+or+(select+left(password,1)+from+challenge_users_6WDzKXNcjaYiNPkSr+where+userid=‘tom‘)=‘a‘+--
写一个脚本就可以得到密码 thisisasecretfortomonly了
Admin password reset
还是老问题,收不到邮件,这个题目是让重置admin
用户的密码,当输入邮箱后在WebWolf里什么也看不到,后发现发邮件时必须包含你当前的用户名,比如我的用户名是admin1,则应该给用户admin1@xxx.xxx
发送。
接受到邮件后,点击reset
链接。
说不是admin用户,则说明此题我们需要构造出用户admin
的重置链接。在题目链接下测试了一下.git
文件后,链接http://127.0.0.1:8080/WebGoat/challenge/7/.git
,打开发现是git的包,则回复使用一下。
将下载的git文件解压,然后打开命令开,使用git status
来看一下状态。
这里获取文件后,可以通过jd-gui反编译PasswordResetLink.class
看到源代码。
/**
* WARNING: DO NOT CHANGE FILE WITHOUT CHANGING .git contents
*
* @author nbaars
* @since 8/17/17.
*/
public class PasswordResetLink {
public String createPasswordReset(String username, String key) {
Random random = new Random();
if (username.equalsIgnoreCase("admin")) {
//Admin has a fix reset link
random.setSeed(key.length());
}
return scramble(random, scramble(random, scramble(random, MD5.getHashString(username))));
}
public static String scramble(Random random, String inputString) {
char a[] = inputString.toCharArray();
for (int i = 0; i < a.length; i++) {
int j = random.nextInt(a.length);
char temp = a[i];
a[i] = a[j];
a[j] = temp;
}
return new String(a);
}
public static void main(String[] args) {
if (args == null || args.length != 2) {
System.out.println("Need a username and key");
System.exit(1);
}
String username = args[0];
String key = args[1];
System.out.println("Generation password reset link for " + username);
System.out.println("Created password reset link: " + new PasswordResetLink().createPasswordReset(username, key));
}
}
关键代码在createPasswordReset
中,可以发现是admin
用户时,会把一个key的长度传进Random函数里当做种子进行计算,所以就好办了,按照createPasswordReset
的算法,将key的长度多尝试几次,如1-30,然后使用点击访问链接就可以了。
经过实际的工作,写来一段代码来看不同值的hash值。
发现当用户为admin是,上图两个红框内的内容是一样的,按道理说是符合题目要求的,但是webgoate官方给出的hash为375afe1104f4a487a73823c50a9292a2
,应该是答案出了问题。
Without account
本题目需要用户进行投票,如果成功,则功能通过,但是需要登录后才能进行投票。
下面是获取flag的关键代码
@GetMapping(value = "/vote/{stars}", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public ResponseEntity<?> vote(@PathVariable(value = "stars") int nrOfStars, HttpServletRequest request) {
//Simple implementation of VERB Based Authentication
String msg = "";
if (request.getMethod().equals("GET")) {
HashMap<String, Object> json = Maps.newHashMap();
json.put("error", true);
json.put("message", "Sorry but you need to login first in order to vote");
return ResponseEntity.status(200).body(json);
}
Integer allVotesForStar = votes.getOrDefault(nrOfStars, 0);
votes.put(nrOfStars, allVotesForStar + 1);
return ResponseEntity.ok().header("X-Flag", "Thanks for voting, your flag is: " + Flag.FLAGS.get(8)).build();
}
查看了源码后发现,只要是GET请求都会返回失败。但这个GetMapping就是get提交的,所以我的思路是,使用其他方法提交请求绕过,先将GET改为POST提交。
失败了。
然后换成PUT,还是失败,之后换成HEAD,发现了flag。(就是这么简单)
所以这道题主要还是考对http协议的熟悉,假如就只知道提交方式GET,POST,PUT是做不出来的。