This commit is contained in:
nitowa
2022-04-09 14:37:20 +02:00
commit 0086c95dc0
21 changed files with 5836 additions and 0 deletions
Executable
+7
View File
@@ -0,0 +1,7 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
spec: "@yarnpkg/plugin-typescript"
yarnPath: .yarn/releases/yarn-berry.cjs
Executable
+16
View File
@@ -0,0 +1,16 @@
FROM node:lts
RUN apt-get update && apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2
WORKDIR /problem
ADD .yarnrc.yml .
ADD .yarn ./.yarn/
ADD package.json .
ADD yarn.lock .
RUN yarn
ADD . .
RUN yarn build
CMD ["yarn", "start"]
+24
View File
@@ -0,0 +1,24 @@
<html>
<head>
<link rel="stylesheet" href="/css/common.css">
<link rel="stylesheet" href="/css/calc.css">
<script id="program" language="json" type="{{ content-type }}">
{{ program }}
</script>
<script type="module">
window.addEventListener("load", () => {
import("/js/calc.mjs");
})
</script>
</head>
<body>
<div class="body">
<h1 class="title" id="name">Loading</h1>
<div id="input"></div>
<span id="output"></span>
<span id="error"></span>
<button id="report">Show me your math!</span>
</div>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
.body #input {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.body #input > input {
justify-self: center;
outline: none;
padding: 16px;
margin: 16px;
font-size: 24px;
border: 2px solid #2a2a2a;
width: 128px;
height: 64px;
border-radius: 16px;
text-align: center;
}
.body #output {
font-size: 24px;
text-align: center;
}
.body #output:empty {
display: none;
}
.body #output::before {
content: "Result:";
color: #2a2a2a;
padding-right: 0.5em;
}
.body #error {
font-size: 24px;
text-align: center;
color: #e21c1c;
}
.body #error:empty {
display: none;
}
.body #error::before {
content: "Error:";
color: #e94e4e;
padding-right: 0.5em;
}
+21
View File
@@ -0,0 +1,21 @@
body {
font-family: 'Oxygen';
}
.body {
display: flex;
max-width: 800px;
margin: auto;
flex-direction: column;
}
.body > * {
margin: 8px 0px;
}
.body .title {
justify-content: center;
display: flex;
flex-direction: row;
}
+9
View File
@@ -0,0 +1,9 @@
.body #name {
max-width: 400px;
font-size: 24px;
align-self: center;
}
.body #program {
resize: vertical;
}
+28
View File
@@ -0,0 +1,28 @@
<html>
<head>
<link rel="stylesheet" href="/css/common.css">
<link rel="stylesheet" href="/css/main.css">
<script type="module">
window.addEventListener("load", () => {
import("/js/main.mjs");
})
</script>
</head>
<body>
<div class="body">
<h1 class="title">Upload your Program to Run</h1>
<input id="name" placeholder="New Program"/>
<textarea id="program" style="min-height: 200px;">
(-b + sqrt(b^2 - 4a*c)) / 2a
</textarea>
<div>
Choose a program type:
<select id="content-type">
<option value="application/x-yaca-ast">AST</option>
<option value="application/x-yaca-code" selected>Code</option>
</select>
</div>
<button id="upload">Upload</button>
</div>
</body>
</html>
+106
View File
@@ -0,0 +1,106 @@
const allowedMathFunctions = new Set([
"abs",
"acos",
"asin",
"atan",
"cos",
"sin",
"tan",
"ceil",
"floor",
"exp",
"log",
"log2",
"log10",
"sqrt",
]);
export default function astToJs(ast) {
if (typeof ast !== "object" || ast === null) {
throw new Error("Ast node must be an object");
}
switch (ast.kind) {
case "number": {
if (typeof ast.value !== "number") {
throw new Error("Number is of the wrong type");
}
return {
code: `${ast.value}`,
variables: new Set(),
};
}
case "variable": {
if (typeof ast.variable !== "string") {
throw new Error("Variable name not specified");
}
if (!ast.variable.match(/^[a-z][a-z0-9_]*$/)) {
throw new Error(`Invalid variable name: ${ast.variable}`);
}
const name = `var_${ast.variable}`;
return {
code: name,
variables: new Set([name]),
}
}
case "function": {
const { name, argument } = ast;
const { code: argumentCode, variables } = astToJs(argument);
if (typeof name !== "string") {
throw new Error("Function name must be a string");
}
if (!allowedMathFunctions.has(name)) {
throw new Error(`Invalid function: ${name}`);
}
const code = `Math.${name}(${argumentCode})`;
return { code, variables };
}
case "unop": {
const { code: nestedCode, variables } = astToJs(ast.value);
const op =
ast.op === "negate" ? "-" :
ast.op === "invert" ? "~" :
null;
if (op === null) {
throw new Error("Invalid unary operator");
}
const code = `${op}(${nestedCode})`;
return { code, variables };
}
case "binop":
{
const [left, right] = ast.values;
const leftResult = astToJs(left);
const rightResult = astToJs(right);
const op =
ast.op === "add" ? "+" :
ast.op === "subtract" ? "-" :
ast.op === "multiply" ? "*" :
ast.op === "divide" ? "/" :
ast.op === "exponent" ? "**" :
null; // null: never
if (op === null) {
throw new Error("Invalid binary operator");
}
return {
code: `(${leftResult.code} ${op} ${rightResult.code})`,
variables: new Set([...leftResult.variables, ...rightResult.variables]),
}
}
default: {
throw new Error(`Unknown ast kind: ${ast.kind}`);
}
}
}
+44
View File
@@ -0,0 +1,44 @@
import astToJs from "/js/ast-to-js.mjs";
import evalCode from "/js/eval-code.mjs";
import lex from "/js/lex.mjs";
import parse from "/js/parse.mjs";
const $ = document.querySelector.bind(document);
const nameEl = $("#name");
const errorEl = $("#error");
const reportEl = $("#report");
const astProgram = $("#program");
const program = JSON.parse(astProgram.textContent);
nameEl.innerText = `Running: ${program.name}`;
reportEl.addEventListener("click", () => {
const file = location.pathname.split("/").slice(-1)[0];
reportEl.disabled = true;
reportEl.innerText = "Reported!";
fetch(`/report`, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ file })
});
})
try {
let ast;
if (astProgram.type === "application/x-yaca-code") {
const tokens = lex(program.code);
ast = parse(tokens);
} else {
ast = JSON.parse(program.code);
}
const jsProgram = astToJs(ast);
evalCode(jsProgram);
} catch(e) {
console.error(e);
const msg = e instanceof Error ? e.message : "Something went wrong";
errorEl.innerText = msg;
}
+64
View File
@@ -0,0 +1,64 @@
const $ = document.querySelector.bind(document);
const prepareInputs = (variables, onChange) => {
const inputEl = $("#input");
for (const node of [...inputEl.childNodes]) {
node.remove();
}
for (const variable of variables) {
const input = document.createElement("input");
input.setAttribute("type", "text");
input.setAttribute("placeholder", `${variable.slice(4)}`);
input.setAttribute("value", "");
input.addEventListener("keyup", () => {
let value = Number(input.value);
if (isNaN(value)) {
value = 0;
}
onChange(variable, value);
});
inputEl.appendChild(input);
}
}
const updateOutput = (result) => {
// Float truncation
if (typeof result === "number" && Math.floor(result) !== result) {
result = result.toFixed(2);
}
const $ = document.querySelector.bind(document);
const outputEl = $("#output");
outputEl.innerText = result;
}
export default ({ code, variables }) => {
const varList = [...variables].sort((a, b) => a.localeCompare(b));
const fn = new Function(...varList, `return (${code});`);
const values = new Map(varList.map((val) => [val, 0]));
const refresh = () => {
const result = fn(...varList.map((val) => values.get(val)));
if (isNaN(result)) {
throw new Error("Output was not a number")
}
$("#error").innerText = "";
updateOutput(result);
}
const onChange = (variable, value) => {
values.set(variable, value);
refresh();
}
prepareInputs(varList, onChange);
refresh();
}
+86
View File
@@ -0,0 +1,86 @@
export default (source) => {
let index = 0;
const tokens = [];
while (index < source.length) {
const token = source[index];
index += 1;
switch (token) {
case ' ':
case '\t':
case '\n':
case '\r':
break;
case "~":
case "+":
case "-":
case "*":
case "/":
case "^": {
const op =
token === "~" ? "invert" :
token === "+" ? "add" :
token === "-" ? "subtract" :
token === "*" ? "multiply" :
token === "/" ? "divide" :
token === "^" ? "exponent" :
null;
tokens.push({
kind: "operator",
value: op,
});
break
}
case "(": {
tokens.push({
kind: "open-paren",
});
break;
}
case ")": {
tokens.push({
kind: "close-paren",
});
break;
}
default: {
if (token.match(/^[0-9\.]$/)) {
let currentToken = token;
while (index < source.length && source[index].match(/^[0-9\.]$/)) {
currentToken += source[index];
index += 1;
}
const value = Number(currentToken);
tokens.push({
kind: "number",
value,
});
break;
}
if (token.match(/^[a-z]$/)) {
let currentToken = token;
while (index < source.length && source[index].match(/^[a-z0-9_]$/)) {
currentToken += source[index];
index += 1;
}
tokens.push({
kind: "variable",
value: currentToken,
});
break;
}
throw new Error(`Syntax error: Unexpected "${token}"`)
}
}
}
return tokens;
}
+98
View File
@@ -0,0 +1,98 @@
const $ = document.querySelector.bind(document);
const nameEl = $("#name");
const uploadEl = $("#upload");
const programEl = $("#program");
const contentTypeEl = $("#content-type");
uploadEl.addEventListener("click", async () => {
const code = programEl.value;
// We use logical or instead of nullish coalescing because
// [input] values are always coerced to a string
const name = nameEl.value || nameEl.getAttribute("placeholder");
const type = contentTypeEl.value;
const res = await fetch("/upload", {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({ type, program: { name, code } })
});
const url = await res.text();
window.location.href = url;
});
const staticAst = `{
"kind": "binop",
"op": "divide",
"values": [
{
"kind": "binop",
"op": "add",
"values": [
{
"kind": "unop",
"op": "negate",
"value": { "kind": "variable", "variable": "b"}
},
{
"kind": "function",
"name": "sqrt",
"argument": {
"kind": "binop",
"op": "subtract",
"values": [
{
"kind": "binop",
"op": "exponent",
"values": [
{ "kind": "variable", "variable": "b"},
{ "kind": "number", "value": 2}
]
},
{
"kind": "binop",
"op": "multiply",
"values": [
{ "kind": "number", "value": 4 },
{
"kind": "binop",
"op": "multiply",
"values": [
{ "kind": "variable", "variable": "a" },
{ "kind": "variable", "variable": "c" }
]
}
]
}
]
}
}
]
},
{
"kind": "binop",
"op": "multiply",
"values": [
{ "kind": "number", "value": 2 },
{ "kind": "variable", "variable": "a" }
]
}
]
}`;
const staticCode = `(-b + sqrt(b^2 - 4a*c)) / 2a`;
const resetTextArea = () => {
const contentType = contentTypeEl.value;
if (contentType === "application/x-yaca-ast") {
programEl.value = staticAst;
} else {
programEl.value = staticCode;
}
}
contentTypeEl.addEventListener("change", resetTextArea);
resetTextArea();
+234
View File
@@ -0,0 +1,234 @@
/*
Operation precedences:
0: exponentiation
1: implicit multiplication
2: multiplication/division
3: unary operators
4: addition/subtraction
*/
// We could probably have lexed directly into the parse token, but
// this way we keep a bit more separation of church & state
const lexTokenToParseToken = (token) => {
if (token === undefined) {
return { kind: "EOF" };
}
switch (token.kind) {
case "operator": {
switch (token.value) {
case "invert": return { kind: "UNOP", value: token.value, precedence: 3, isOp: true };
case "subtract": return { kind: "MAYBE_UNOP", value: token.value, unopValue: "negate", precedence: 4, isOp: true };
case "exponent": return { kind: "BINOP", value: token.value, precedence: 0, isOp: true, };
case "multiply": return { kind: "BINOP", value: token.value, precedence: 2, isOp: true, };
case "divide": return { kind: "BINOP", value: token.value, precedence: 2, isOp: true, };
case "add": return { kind: "BINOP", value: token.value, precedence: 4, isOp: true, };
default: throw new Error(`Unknown operator ${token.value}`);
}
}
case "open-paren": return { kind: "EXPR_START" };
case "close-paren": return { kind: "EXPR_END" };
case "number": return { kind: "VALUE", ast: { kind: "number", value: token.value } };
case "variable": return { kind: "VALUE", ast: { kind: "variable", variable: token.value } };
default: throw new Error(`Unknown token kind ${token.kind}`);
}
}
const mightBeUnop = (token) => {
return (
token.kind === "UNOP"
|| token.kind === "MAYBE_UNOP"
|| token.kind === "FUNCTION"
|| token.kind === "IMPLICIT_MULTIPLICATION"
);
}
const mightBeBinop = (token) => {
return (
token.kind === "BINOP"
|| token.kind === "MAYBE_UNOP"
);
}
const parseOne = (stack, lookahead) => {
// If we can reduce a parenthetical expression, we want to
if (stack[0]?.kind === "EXPR_END") {
const [_end, value, _start, ...rest] = stack;
if (stack[1]?.kind !== "VALUE" || stack[2]?.kind !== "EXPR_START") {
throw new Error("Received unexpected close parenthesis");
}
return ["reduce", [value, ...rest]];
}
// Otherwise, all of our reductions occur on values
if (stack[0]?.kind === "VALUE") {
// We have some special cases for two adjacent values, when the first one is
// a pure variable (function call) or number (term multiplication)
if (lookahead.kind === "VALUE" || lookahead.kind === "EXPR_START") {
if (stack[0].ast.kind === "variable") {
const [token, ...rest] = stack;
const newToken = {
kind: "FUNCTION",
isOp: true,
precedence: 3,
name: token.ast.variable
}
const newStack = [newToken, ...rest];
return ["reduce", newStack];
}
if (stack[0].ast.kind === "number") {
const [token, ...rest] = stack;
const newToken = {
kind: "IMPLICIT_MULTIPLICATION",
isOp: true,
precedence: 1,
value: token.ast.value
}
const newStack = [newToken, ...rest];
return ["reduce", newStack];
}
}
// If we have an operator on our stack, we want to reduce it unless the upcoming
// operator has a stronger precedence
if (stack[1]?.isOp) {
const shouldShift = mightBeBinop(lookahead) && lookahead.precedence < stack[1].precedence;
if (shouldShift) {
return ["shift"];
}
const isBinop = mightBeBinop(stack[1]) && stack[2]?.kind === "VALUE";
if (isBinop) {
const [right, binop, left, ...rest] = stack;
// Binops are easy
const newValue = {
kind: "VALUE",
ast: {
kind: "binop",
op: binop.value,
values: [
left.ast,
right.ast,
]
}
};
return ["reduce", [newValue, ...rest]]
}
const isUnop = mightBeUnop(stack[1]);
if (!isUnop) {
throw new Error("Unexpected operator");
}
const [value, unop, ...rest] = stack;
let newToken;
// Handle each of the different types of unary operators
switch (unop.kind) {
case "UNOP":
case "MAYBE_UNOP": {
const op = unop.kind === "MAYBE_UNOP" ? unop.unopValue : unop.value;
newToken = {
kind: "VALUE",
ast: {
kind: "unop",
op,
value: value.ast
}
};
break;
}
case "FUNCTION": {
newToken = {
kind: "VALUE",
ast: {
kind: "function",
name: unop.name,
argument: value.ast,
}
};
break;
}
case "IMPLICIT_MULTIPLICATION": {
newToken = {
kind: "VALUE",
ast: {
kind: "binop",
op: "multiply",
values: [
{ kind: "number", value: unop.value },
value.ast,
]
}
};
break
}
default: {
throw new Error(`Unknown unary operator: ${unop.kind}`);
}
}
return ["reduce", [newToken, ...rest]];
}
}
// Otherwise, we have no reductions to do, so we shift in a new token
return ["shift"];
}
export default (tokens) => {
const queue = [...tokens];
let stack = [];
const maxIter = 1000;
let iter = 0;
while (queue.length > 0 || stack.length > 1) {
// I haven't proven that this terminates so uh
// Hopefully this will keep me from nuking anyone's chrome
if (iter >= maxIter) {
throw new Error("Timeout");
}
iter++;
const lookahead = lexTokenToParseToken(queue[0]);
const action = parseOne(stack, lookahead);
if (window.DEBUG) {
console.log([...stack], lookahead, action);
}
switch (action[0]) {
case "shift": {
if (lookahead.kind === "EOF") {
throw new Error("Attempting to shift EOF, which indicates a malformed program");
}
queue.shift();
stack = [lookahead, ...stack]
break;
}
case "reduce": {
stack = action[1];
}
}
}
// If we parsed correctly, we should be left with a single value
// representing our final result
if (stack[0]?.kind !== "VALUE") {
throw new Error("Parser did not return a value");
}
return stack[0].ast;
}
+1996
View File
File diff suppressed because it is too large Load Diff
Executable
+26
View File
@@ -0,0 +1,26 @@
{
"name": "yaca",
"packageManager": "yarn@3.1.0",
"devDependencies": {
"@types/body-parser": "^1.19.2",
"@types/express": "^4.17.13",
"@types/fs-extra": "^9.0.13",
"@types/puppeteer": "^5.4.5",
"@types/sqlite3": "^3.1.8",
"@types/uuid": "^8",
"typescript": "^4.5.2"
},
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"fs-extra": "^10.0.0",
"puppeteer": "^13.5.2",
"sqlite3": "^5.0.2",
"uuid": "^8.3.2"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"bundle": "bash -c 'tar -cvzf ../../bundle.tgz .yarn/{plugins,releases} .yarnrc.yml client src Dockerfile package.json tsconfig.json yarn.lock'"
}
}
+73
View File
@@ -0,0 +1,73 @@
import * as sqlite from "sqlite3";
import * as path from "path";
const db = new sqlite.Database(path.resolve("./queue.sqlite"));
const run = <T>(sql: string, params: unknown[]) => {
return new Promise<T[]>((resolve, reject) => {
db.all(sql, ...params, (err: unknown, rows: T[]) => {
if (err) {
return reject(err);
}
resolve(rows);
});
});
}
export async function setup(){
await run(`
CREATE TABLE IF NOT EXISTS queue (
id SERIAL PRIMARY KEY,
url TEXT NOT NULL,
ip TEXT NOT NULL
);
`, []);
}
export const enqueue = async (url: string, ip: string) => {
const [{ count }] = await run<{ count: number }>(`
SELECT count(*) as count
FROM queue
WHERE ip = ?;
`, [ip]);
if (count > 3) {
return false;
}
await run(`
INSERT INTO queue (url, ip)
VALUES (?, ?);
`, [url, ip]);
const [{ count: queueLength }] = await run<{ count: number }>(`
SELECT count(*) as count
FROM queue;
`, [])
return queueLength;
}
export const dequeue = async () => {
const request = await run<{ url: string, ip: string }>(`
SELECT url, ip
FROM queue
ORDER BY id ASC
LIMIT 1;
`, []);
if (request.length === 0) {
return undefined;
}
const [{ url, ip }] = request;
// Delete based off of url and ip in case there are duplicates
await run(`
DELETE FROM queue
WHERE url = ? AND ip = ?;
`, [url, ip]);
return url;
}
Executable
+97
View File
@@ -0,0 +1,97 @@
import * as express from "express";
import * as fs from "fs-extra";
import * as path from "path";
import * as bodyParser from "body-parser";
import { v4 as uuid } from "uuid";
import { startVisiting } from "./page-worker";
import { enqueue, setup } from "./database";
const cacheDir = path.join(__dirname, "../cache");
const clientDir = path.join(__dirname, "../client");
const main = async () => {
await setup()
await fs.ensureDir(cacheDir);
const app = express();
app.use(bodyParser.json());
app.use((req, res, next) => {
res.setHeader("Content-Security-Policy", "script-src 'self' 'unsafe-eval' 'unsafe-inline'");
next();
});
app.get("/", (req, res) => {
res.sendFile(path.join(clientDir, "index.html"));
});
app.post("/upload", async (req, res) => {
if (typeof req.body !== "object") {
return res.status(500).send("Bad payload");
}
const { type, program } = req.body;
if (
typeof type !== "string"
|| type.match(/^[a-zA-Z\-/]{3,}$/) === null
|| typeof program.name !== "string"
|| typeof program.code !== "string"
|| program.code.length > 10000
) {
return res.status(500).send("Invalid program");
}
const sanitizedProgram =
JSON.stringify(program)
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
const template = await fs.readFile(path.join(clientDir, "calculator.hbs"), "utf-8");
const formattedFile =
template
.replace("{{ content-type }}", type)
.replace("{{ program }}", sanitizedProgram);
const fileName = `program-${uuid()}`;
await fs.writeFile(path.join(cacheDir, fileName), formattedFile);
res.send(`/program/${fileName}`);
});
app.post("/report", async (req, res) => {
if (
typeof req.body !== "object"
|| typeof req.body.file !== "string"
|| !req.body.file.match(/^program-[a-f0-9-]+$/)
) {
return res.status(500).send("Bad payload");
}
const ip = req.ip;
const { file } = req.body;
const url = `http://localhost:3838/program/${file}`;
await enqueue(url, ip)
res.send("Ok");
})
app.get("/program/:file", async (req, res) => {
const fileName = req.params.file;
const filePath = path.join(cacheDir, fileName);
res.type("html");
res.sendFile(filePath);
});
app.use("/js", express.static(path.join(clientDir, "js")));
app.use("/css", express.static(path.join(clientDir, "css")));
app.listen(3838, () => {
console.log("Listening on port 3838");
});
}
startVisiting();
main();
+48
View File
@@ -0,0 +1,48 @@
import * as puppeteer from "puppeteer";
import * as fs from "fs/promises";
import * as path from "path";
import { dequeue } from "./database";
const flag = process.env.FLAG ?? "flag{missing}"
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const visitOne = async () => {
const url = await dequeue();
if (url === undefined) {
await sleep(500);
return;
}
const browser = await puppeteer.launch({
dumpio: true,
pipe: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setCookie({
name: "flag",
value: flag,
domain: "localhost:3838",
})
await Promise.race([
page.goto(url),
sleep(3000),
]);
await sleep(3000);
await browser.close();
}
export const startVisiting = async () => {
while (true) {
try {
await visitOne();
} catch (e) {
console.error(e);
}
}
}
Executable
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"strict": true,
"module": "CommonJS",
"target": "ES2020",
"declaration": true,
"incremental": true,
"sourceMap": true,
"outDir": "dist"
},
"include": ["src"]
}
+39
View File
@@ -0,0 +1,39 @@
# Plaid CTF: Yet Another Calculator App
Participant: Peter Millauer / nitowa (01350868)
## TL;DR / Short Summary
Classical XSS web exploit. The solution used special string replacement patterns to break out of string escapes.
## Task Description
## Analysis Steps
Explain your analysis in detail. Cover all the technical aspects, including the used tools and commands. Mention other collaborators and distinguish contributions.
## Vulnerabilities / Exploitable Issue(s)
List security issues you discovered in the scope of the task and how they could be exploited.
## Solution
Provide a clean (i.e., without analysis and research steps) guideline to get from the task description to the solution. If you did not finish the task, take your most promising approach as a goal.
## Failed Attempts
Describe attempts apart from the solution above which you tried. Recap and try to explain why they did not work.
## Alternative Solutions
If you can think of an alternative solution (or there are others already published), compare your attempts with those.
## Lessons Learned
Document what you learned during the competition.
## References
List external resources (academic papers, technical blogs, CTF writeups, ...) you used while working on this task.
Executable
+2759
View File
File diff suppressed because it is too large Load Diff